From 0966d1d282b04cec6f3b0d34a4ba73dae2d08d56 Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Fri, 3 Mar 2023 02:57:38 +0800 Subject: [PATCH] feat: improve `bind:group` behavior (#7892) track all `#each` variables that could result in a change to the inputs and also update the `$$binding_groups` variable which holds the references to the inputs of each group accordingly. Fixes #7633 Fixes #6112 Fixes #7884 --- src/compiler/compile/render_dom/Block.ts | 10 +- src/compiler/compile/render_dom/Renderer.ts | 15 ++- .../render_dom/wrappers/Element/Attribute.ts | 12 ++ .../render_dom/wrappers/Element/Binding.ts | 122 +++++++++++------- .../render_dom/wrappers/Element/index.ts | 2 + src/runtime/internal/dom.ts | 63 +++++++-- src/runtime/internal/keyed_each.ts | 7 +- .../binding-input-group-each-10/_config.js | 52 ++++++++ .../binding-input-group-each-10/main.svelte | 36 ++++++ .../binding-input-group-each-11/_config.js | 88 +++++++++++++ .../binding-input-group-each-11/main.svelte | 60 +++++++++ .../binding-input-group-each-12/_config.js | 89 +++++++++++++ .../binding-input-group-each-12/main.svelte | 60 +++++++++ .../binding-input-group-each-13/_config.js | 30 +++++ .../binding-input-group-each-13/main.svelte | 10 ++ .../binding-input-group-each-8/_config.js | 78 +++++++++++ .../binding-input-group-each-8/main.svelte | 36 ++++++ .../binding-input-group-each-9/_config.js | 49 +++++++ .../binding-input-group-each-9/main.svelte | 36 ++++++ 19 files changed, 791 insertions(+), 64 deletions(-) create mode 100644 test/runtime/samples/binding-input-group-each-10/_config.js create mode 100644 test/runtime/samples/binding-input-group-each-10/main.svelte create mode 100644 test/runtime/samples/binding-input-group-each-11/_config.js create mode 100644 test/runtime/samples/binding-input-group-each-11/main.svelte create mode 100644 test/runtime/samples/binding-input-group-each-12/_config.js create mode 100644 test/runtime/samples/binding-input-group-each-12/main.svelte create mode 100644 test/runtime/samples/binding-input-group-each-13/_config.js create mode 100644 test/runtime/samples/binding-input-group-each-13/main.svelte create mode 100644 test/runtime/samples/binding-input-group-each-8/_config.js create mode 100644 test/runtime/samples/binding-input-group-each-8/main.svelte create mode 100644 test/runtime/samples/binding-input-group-each-9/_config.js create mode 100644 test/runtime/samples/binding-input-group-each-9/main.svelte diff --git a/src/compiler/compile/render_dom/Block.ts b/src/compiler/compile/render_dom/Block.ts index 164e938674..c40dedc3b5 100644 --- a/src/compiler/compile/render_dom/Block.ts +++ b/src/compiler/compile/render_dom/Block.ts @@ -1,4 +1,4 @@ -import Renderer from './Renderer'; +import Renderer, { BindingGroup } from './Renderer'; import Wrapper from './wrappers/shared/Wrapper'; import { b, x } from 'code-red'; import { Node, Identifier, ArrayPattern } from 'estree'; @@ -40,6 +40,7 @@ export default class Block { bindings: Map; binding_group_initialised: Set = new Set(); + binding_groups: Set = new Set(); chunks: { declarations: Array; @@ -249,6 +250,7 @@ export default class Block { } } + this.render_binding_groups(); this.render_listeners(); const properties: Record = {}; @@ -500,4 +502,10 @@ export default class Block { } } } + + render_binding_groups() { + for (const binding_group of this.binding_groups) { + binding_group.render(); + } + } } diff --git a/src/compiler/compile/render_dom/Renderer.ts b/src/compiler/compile/render_dom/Renderer.ts index c00ef95586..ad2d1b092f 100644 --- a/src/compiler/compile/render_dom/Renderer.ts +++ b/src/compiler/compile/render_dom/Renderer.ts @@ -22,6 +22,15 @@ type BitMasks = Array<{ names: string[]; }>; +export interface BindingGroup { + binding_group: (to_reference?: boolean) => Node; + contexts: string[]; + list_dependencies: Set; + keypath: string; + elements: Identifier[]; + render: () => void; +} + export default class Renderer { component: Component; // TODO Maybe Renderer shouldn't know about Component? options: CompileOptions; @@ -33,7 +42,7 @@ export default class Renderer { blocks: Array = []; readonly: Set = new Set(); meta_bindings: Array = []; // initial values for e.g. window.innerWidth, if there's a meta tag - binding_groups: Map Node; is_context: boolean; contexts: string[]; index: number; keypath: string }> = new Map(); + binding_groups: Map = new Map(); block: Block; fragment: FragmentWrapper; @@ -64,10 +73,6 @@ export default class Renderer { this.add_to_context('#slots'); } - if (this.binding_groups.size > 0) { - this.add_to_context('$$binding_groups'); - } - // main block this.block = new Block({ renderer: this, diff --git a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts index ca35ea84db..6cb3a00218 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/Attribute.ts @@ -85,6 +85,7 @@ export default class AttributeWrapper extends BaseAttributeWrapper { if (node.name === 'value') { handle_select_value_binding(this, node.dependencies); + this.parent.has_dynamic_value = true; } } @@ -180,6 +181,17 @@ export default class AttributeWrapper extends BaseAttributeWrapper { `; } + if (this.node.name === 'value' && dependencies.length > 0) { + if (this.parent.bindings.some(binding => binding.node.name === 'group')) { + this.parent.dynamic_value_condition = block.get_unique_name('value_has_changed'); + block.add_variable(this.parent.dynamic_value_condition, x`false`); + updater = b` + ${updater} + ${this.parent.dynamic_value_condition} = true; + `; + } + } + if (dependencies.length > 0) { const condition = this.get_dom_update_conditions(block, block.renderer.dirty(dependencies)); diff --git a/src/compiler/compile/render_dom/wrappers/Element/Binding.ts b/src/compiler/compile/render_dom/wrappers/Element/Binding.ts index 47e3795ec2..e48f3e0030 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/Binding.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/Binding.ts @@ -5,7 +5,7 @@ import InlineComponentWrapper from '../InlineComponent'; import get_object from '../../../utils/get_object'; import replace_object from '../../../utils/replace_object'; import Block from '../../Block'; -import Renderer from '../../Renderer'; +import Renderer, { BindingGroup } from '../../Renderer'; import flatten_reference from '../../../utils/flatten_reference'; import { Node, Identifier } from 'estree'; import add_to_set from '../../../utils/add_to_set'; @@ -26,6 +26,7 @@ export default class BindingWrapper { snippet: Node; is_readonly: boolean; needs_lock: boolean; + binding_group: BindingGroup; constructor(block: Block, node: Binding, parent: ElementWrapper | InlineComponentWrapper) { this.node = node; @@ -45,6 +46,10 @@ export default class BindingWrapper { this.object = get_object(this.node.expression.node).name; + if (this.node.name === 'group') { + this.binding_group = get_binding_group(parent.renderer, this, block); + } + // view to model this.handler = get_event_handler(this, parent.renderer, block, this.object, this.node.raw_expression); @@ -67,6 +72,10 @@ export default class BindingWrapper { } }); + if (this.binding_group) { + this.binding_group.list_dependencies.forEach(dep => dependencies.add(dep)); + } + return dependencies; } @@ -105,6 +114,7 @@ export default class BindingWrapper { const update_conditions: any[] = this.needs_lock ? [x`!${lock}`] : []; const mount_conditions: any[] = []; + let update_or_condition: any = null; const dependency_array = Array.from(this.get_dependencies()); @@ -142,33 +152,12 @@ export default class BindingWrapper { switch (this.node.name) { case 'group': { - const { binding_group, is_context, contexts, index, keypath } = get_binding_group(parent.renderer, this.node, block); - block.renderer.add_to_context('$$binding_groups'); + this.binding_group.elements.push(this.parent.var); - if (is_context && !block.binding_group_initialised.has(keypath)) { - if (contexts.length > 1) { - let binding_group = x`${block.renderer.reference('$$binding_groups')}[${index}]`; - for (const name of contexts.slice(0, -1)) { - binding_group = x`${binding_group}[${block.renderer.reference(name)}]`; - block.chunks.init.push( - b`${binding_group} = ${binding_group} || [];` - ); - } - } - block.chunks.init.push( - b`${binding_group(true)} = [];` - ); - block.binding_group_initialised.add(keypath); + if ((this.parent as ElementWrapper).has_dynamic_value) { + update_or_condition = (this.parent as ElementWrapper).dynamic_value_condition; } - - block.chunks.hydrate.push( - b`${binding_group(true)}.push(${parent.var});` - ); - - block.chunks.destroy.push( - b`${binding_group(true)}.splice(${binding_group(true)}.indexOf(${parent.var}), 1);` - ); break; } @@ -214,7 +203,8 @@ export default class BindingWrapper { if (update_dom) { if (update_conditions.length > 0) { - const condition = update_conditions.reduce((lhs, rhs) => x`${lhs} && ${rhs}`); + let condition = update_conditions.reduce((lhs, rhs) => x`${lhs} && ${rhs}`); + if (update_or_condition) condition = x`${update_or_condition} || (${condition})`; block.chunks.update.push(b` if (${condition}) { @@ -279,7 +269,8 @@ function get_dom_updater( return b`${element.var}.${binding.node.name} = ${binding.snippet};`; } -function get_binding_group(renderer: Renderer, value: Binding, block: Block) { +function get_binding_group(renderer: Renderer, binding: BindingWrapper, block: Block) { + const value = binding.node; const { parts } = flatten_reference(value.raw_expression); let keypath = parts.join('.'); @@ -314,41 +305,75 @@ function get_binding_group(renderer: Renderer, value: Binding, block: Block) { contexts.push(name); } + // create a global binding_group across blocks if (!renderer.binding_groups.has(keypath)) { const index = renderer.binding_groups.size; + // the bind:group depends on the list in the {#each} block as well + // as reordering (removing and adding back to the DOM) may affect the value + const list_dependencies = new Set(); + let parent = value.parent; + while (parent) { + if (parent.type === 'EachBlock') { + for (const dep of parent.expression.dynamic_dependencies()) { + list_dependencies.add(dep); + } + } + parent = parent.parent; + } + + const elements = []; contexts.forEach(context => { renderer.add_to_context(context, true); }); renderer.binding_groups.set(keypath, { - binding_group: (to_reference: boolean = false) => { - let binding_group = '$$binding_groups'; - let _secondary_indexes = contexts; + binding_group: () => { + let obj = x`$$binding_groups[${index}]`; - if (to_reference) { - binding_group = block.renderer.reference(binding_group); - _secondary_indexes = _secondary_indexes.map(name => block.renderer.reference(name)); - } - - if (_secondary_indexes.length > 0) { - let obj = x`${binding_group}[${index}]`; - _secondary_indexes.forEach(secondary_index => { + if (contexts.length > 0) { + contexts.forEach(secondary_index => { obj = x`${obj}[${secondary_index}]`; }); - return obj; - } else { - return x`${binding_group}[${index}]`; } + return obj; }, - is_context: contexts.length > 0, contexts, - index, - keypath + list_dependencies, + keypath, + elements, + render() { + const local_name = block.get_unique_name('binding_group'); + const binding_group = block.renderer.reference('$$binding_groups'); + block.add_variable(local_name); + if (contexts.length > 0) { + const indexes = { type: 'ArrayExpression', elements: contexts.map(name => block.renderer.reference(name)) }; + block.chunks.init.push( + b`${local_name} = @init_binding_group_dynamic(${binding_group}[${index}], ${indexes})` + ); + block.chunks.update.push( + b`if (${block.renderer.dirty(Array.from(list_dependencies))}) ${local_name}.u(${indexes})` + ); + } else { + block.chunks.init.push( + b`${local_name} = @init_binding_group(${binding_group}[${index}])` + ); + } + block.chunks.hydrate.push( + b`${local_name}.p(${elements})` + ); + block.chunks.destroy.push( + b`${local_name}.r()` + ); + } }); } - return renderer.binding_groups.get(keypath); + // register the binding_group for the block + const binding_group = renderer.binding_groups.get(keypath); + block.binding_groups.add(binding_group); + + return binding_group; } function get_event_handler( @@ -386,7 +411,7 @@ function get_event_handler( } } - const value = get_value_from_dom(renderer, binding.parent, binding, block, contextual_dependencies); + const value = get_value_from_dom(renderer, binding.parent, binding, contextual_dependencies); const mutation = b` ${lhs} = ${value}; @@ -402,10 +427,9 @@ function get_event_handler( } function get_value_from_dom( - renderer: Renderer, + _renderer: Renderer, element: ElementWrapper | InlineComponentWrapper, binding: BindingWrapper, - block: Block, contextual_dependencies: Set ) { const { node } = element; @@ -427,7 +451,7 @@ function get_value_from_dom( // if (name === 'group') { if (type === 'checkbox') { - const { binding_group, contexts } = get_binding_group(renderer, binding.node, block); + const { binding_group, contexts } = binding.binding_group; add_to_set(contextual_dependencies, contexts); return x`@get_binding_group_value(${binding_group()}, this.__value, this.checked)`; } diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index 3472b0612c..eaef3a3b69 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -163,6 +163,8 @@ export default class ElementWrapper extends Wrapper { has_dynamic_attribute: boolean; select_binding_dependencies?: Set; + has_dynamic_value: boolean; + dynamic_value_condition: any; var: any; void: boolean; diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index 942e742e34..70e7dcd7f8 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -13,7 +13,7 @@ export function end_hydrating() { type NodeEx = Node & { claim_order?: number, - hydrate_init? : true, + hydrate_init?: true, actual_end_child?: NodeEx, childNodes: NodeListOf, }; @@ -35,7 +35,7 @@ function init_hydrate(target: NodeEx) { if (target.hydrate_init) return; target.hydrate_init = true; - type NodeEx2 = NodeEx & {claim_order: number}; + type NodeEx2 = NodeEx & { claim_order: number }; // We know that all children have claim_order values since the unclaimed have been detached if target is not let children: ArrayLike = target.childNodes as NodeListOf; @@ -260,7 +260,7 @@ export function listen(node: EventTarget, event: string, handler: EventListenerO } export function prevent_default(fn) { - return function(event) { + return function (event) { event.preventDefault(); // @ts-ignore return fn.call(this, event); @@ -268,7 +268,7 @@ export function prevent_default(fn) { } export function stop_propagation(fn) { - return function(event) { + return function (event) { event.stopPropagation(); // @ts-ignore return fn.call(this, event); @@ -284,14 +284,14 @@ export function stop_immediate_propagation(fn) { } export function self(fn) { - return function(event) { + return function (event) { // @ts-ignore if (event.target === this) fn.call(this, event); }; } export function trusted(fn) { - return function(event) { + return function (event) { // @ts-ignore if (event.isTrusted) fn.call(this, event); }; @@ -359,6 +359,53 @@ export function get_binding_group_value(group, __value, checked) { return Array.from(value); } +export function init_binding_group(group) { + let _inputs: HTMLInputElement[]; + return { + /* push */ p(...inputs: HTMLInputElement[]) { + _inputs = inputs; + _inputs.forEach(input => group.push(input)); + }, + + /* remove */ r() { + _inputs.forEach(input => group.splice(group.indexOf(input), 1)); + } + }; +} + +export function init_binding_group_dynamic(group, indexes: number[]) { + let _group: HTMLInputElement[] = get_binding_group(group); + let _inputs: HTMLInputElement[]; + function get_binding_group(group) { + for (let i = 0; i < indexes.length; i++) { + group = group[indexes[i]] = group[indexes[i]] || []; + } + return group; + } + function push() { + _inputs.forEach(input => _group.push(input)); + } + function remove() { + _inputs.forEach(input => _group.splice(_group.indexOf(input), 1)); + } + return { + /* update */ u(new_indexes: number[]) { + indexes = new_indexes; + const new_group = get_binding_group(group); + if (new_group !== _group) { + remove(); + _group = new_group; + push(); + } + }, + /* push */ p(...inputs: HTMLInputElement[]) { + _inputs = inputs; + push(); + }, + /* remove */ r: remove + }; +} + export function to_number(value) { return value === '' ? null : +value; } @@ -392,7 +439,7 @@ export function children(element: Element) { function init_claim_info(nodes: ChildNodeArray) { if (nodes.claim_info === undefined) { - nodes.claim_info = {last_index: 0, total_claimed: 0}; + nodes.claim_info = { last_index: 0, total_claimed: 0 }; } } @@ -668,7 +715,7 @@ export function toggle_class(element, name, toggle) { element.classList[toggle ? 'add' : 'remove'](name); } -export function custom_event(type: string, detail?: T, { bubbles = false, cancelable = false } = {}): CustomEvent { +export function custom_event(type: string, detail?: T, { bubbles = false, cancelable = false } = {}): CustomEvent { const e: CustomEvent = document.createEvent('CustomEvent'); e.initCustomEvent(type, bubbles, cancelable, detail); return e; diff --git a/src/runtime/internal/keyed_each.ts b/src/runtime/internal/keyed_each.ts index 079d5f4499..a5cfaf7fb6 100644 --- a/src/runtime/internal/keyed_each.ts +++ b/src/runtime/internal/keyed_each.ts @@ -1,4 +1,5 @@ import { transition_in, transition_out } from './transitions'; +import { run_all } from './utils'; export function destroy_block(block, lookup) { block.d(1); @@ -32,6 +33,7 @@ export function update_keyed_each(old_blocks, dirty, get_key, dynamic, ctx, list const new_blocks = []; const new_lookup = new Map(); const deltas = new Map(); + const updates = []; i = n; while (i--) { @@ -43,7 +45,8 @@ export function update_keyed_each(old_blocks, dirty, get_key, dynamic, ctx, list block = create_each_block(key, child_ctx); block.c(); } else if (dynamic) { - block.p(child_ctx, dirty); + // defer updates until all the DOM shuffling is done + updates.push(() => block.p(child_ctx, dirty)); } new_lookup.set(key, new_blocks[i] = block); @@ -99,6 +102,8 @@ export function update_keyed_each(old_blocks, dirty, get_key, dynamic, ctx, list while (n) insert(new_blocks[n - 1]); + run_all(updates); + return new_blocks; } diff --git a/test/runtime/samples/binding-input-group-each-10/_config.js b/test/runtime/samples/binding-input-group-each-10/_config.js new file mode 100644 index 0000000000..edfc508f05 --- /dev/null +++ b/test/runtime/samples/binding-input-group-each-10/_config.js @@ -0,0 +1,52 @@ +// https://github.com/sveltejs/svelte/issues/7633 +export default { + async test({ assert, target, component, window }) { + let inputs = target.querySelectorAll('input'); + + assert.equal(inputs[0].checked, true); + assert.equal(inputs[1].checked, false); + assert.equal(inputs[2].checked, false); + + component.moveDown(0); + component.moveDown(1); + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + ` +
+ b +
+
+ c +
+
+ a +
+ ` + ); + + // after shifting order, should still keep the correct radio checked + inputs = target.querySelectorAll('input'); + assert.equal(inputs[0].checked, false); + assert.equal(inputs[1].checked, false); + assert.equal(inputs[2].checked, true); + + (component.current = 'b'); + await Promise.resolve(); + + inputs = target.querySelectorAll('input'); + assert.equal(inputs[0].checked, true); + assert.equal(inputs[1].checked, false); + assert.equal(inputs[2].checked, false); + + component.moveDown(1); + await Promise.resolve(); + + // after shifting order, should still keep the correct radio checked + inputs = target.querySelectorAll('input'); + assert.equal(inputs[0].checked, true); + assert.equal(inputs[1].checked, false); + assert.equal(inputs[2].checked, false); + } +}; diff --git a/test/runtime/samples/binding-input-group-each-10/main.svelte b/test/runtime/samples/binding-input-group-each-10/main.svelte new file mode 100644 index 0000000000..7c81cada9a --- /dev/null +++ b/test/runtime/samples/binding-input-group-each-10/main.svelte @@ -0,0 +1,36 @@ + + +{#each list as item (item.name)} +
+ {item.name} + {#if true} + + {/if} +
+{/each} + diff --git a/test/runtime/samples/binding-input-group-each-11/_config.js b/test/runtime/samples/binding-input-group-each-11/_config.js new file mode 100644 index 0000000000..a90993861a --- /dev/null +++ b/test/runtime/samples/binding-input-group-each-11/_config.js @@ -0,0 +1,88 @@ +// https://github.com/sveltejs/svelte/issues/6112 +export default { + async test({ assert, target, component, window }) { + let inputs = target.querySelectorAll('input'); + + const check = (set) => { + for (let i = 0; i < inputs.length; i++) { + assert.equal(inputs[i].checked, set.has(i)); + } + }; + + assert.htmlEqual( + target.innerHTML, + ` +
1
+
2 +
+ + +
+
+ + +
+
+
3 +
+ + +
+
+ + +
+
+ ` + ); + + check(new Set([0, 2, 5, 6])); + + const event = new window.Event('change'); + + // dom to value + inputs[3].checked = true; + await inputs[3].dispatchEvent(event); + + check(new Set([0, 3, 5, 6])); + assert.equal(component.pipelineOperations[1].operation.args[1].value, 'd'); + + // remove item + component.pipelineOperations = component.pipelineOperations.slice(1); + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + ` +
2 +
+ + +
+
+ + +
+
+
3 +
+ + +
+
+ + +
+
+ ` + ); + + inputs = target.querySelectorAll('input'); + check(new Set([0, 3, 5, 6])); + + inputs[2].checked = true; + await inputs[2].dispatchEvent(event); + + check(new Set([0, 2, 5, 6])); + } +}; diff --git a/test/runtime/samples/binding-input-group-each-11/main.svelte b/test/runtime/samples/binding-input-group-each-11/main.svelte new file mode 100644 index 0000000000..7c759b5bb4 --- /dev/null +++ b/test/runtime/samples/binding-input-group-each-11/main.svelte @@ -0,0 +1,60 @@ + + +{#each pipelineOperations as { operation, id } (id)} +
+ {id} + {#each operation.args as arg} +
+ {#each arg.options as { value }} + + {/each} +
+ {/each} +
+{/each} diff --git a/test/runtime/samples/binding-input-group-each-12/_config.js b/test/runtime/samples/binding-input-group-each-12/_config.js new file mode 100644 index 0000000000..a3d6c92879 --- /dev/null +++ b/test/runtime/samples/binding-input-group-each-12/_config.js @@ -0,0 +1,89 @@ +// https://github.com/sveltejs/svelte/issues/6112 +export default { + async test({ assert, target, component, window }) { + let inputs = target.querySelectorAll('input'); + + const check = (set) => { + for (let i = 0; i < inputs.length; i++) { + assert.equal(inputs[i].checked, set.has(i)); + } + }; + + assert.htmlEqual( + target.innerHTML, + ` +
1
+
2 +
+ + +
+
+ + +
+
+
3 +
+ + +
+
+ + +
+
+ ` + ); + + check(new Set([0, 2])); + + const event = new window.Event('change'); + + // dom to value + inputs[3].checked = true; + await inputs[3].dispatchEvent(event); + + check(new Set([0, 2, 3])); + assert.deepEqual(component.pipelineOperations[1].operation.args[1].value, ['c', 'd']); + + // remove item + component.pipelineOperations = component.pipelineOperations.slice(1); + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + ` +
2 +
+ + +
+
+ + +
+
+
3 +
+ + +
+
+ + +
+
+ ` + ); + + inputs = target.querySelectorAll('input'); + check(new Set([0, 2, 3])); + + inputs[5].checked = true; + await inputs[5].dispatchEvent(event); + + check(new Set([0, 2, 3, 5])); + assert.deepEqual(component.pipelineOperations[1].operation.args[0].value, ['b']); + } +}; diff --git a/test/runtime/samples/binding-input-group-each-12/main.svelte b/test/runtime/samples/binding-input-group-each-12/main.svelte new file mode 100644 index 0000000000..289353778a --- /dev/null +++ b/test/runtime/samples/binding-input-group-each-12/main.svelte @@ -0,0 +1,60 @@ + + +{#each pipelineOperations as { operation, id } (id)} +
+ {id} + {#each operation.args as arg} +
+ {#each arg.options as { value }} + + {/each} +
+ {/each} +
+{/each} diff --git a/test/runtime/samples/binding-input-group-each-13/_config.js b/test/runtime/samples/binding-input-group-each-13/_config.js new file mode 100644 index 0000000000..affb94b8fb --- /dev/null +++ b/test/runtime/samples/binding-input-group-each-13/_config.js @@ -0,0 +1,30 @@ +export default { + async test({ assert, target, window }) { + const [input1, input2] = target.querySelectorAll('input[type=text]'); + const radio = target.querySelector('input[type=radio]'); + + assert.equal(radio.checked, false); + + const event = new window.Event('input'); + + input1.value = 'world'; + await input1.dispatchEvent(event); + assert.equal(radio.checked, true); + + input2.value = 'foo'; + await input2.dispatchEvent(event); + assert.equal(radio.checked, false); + + input1.value = 'foo'; + await input1.dispatchEvent(event); + assert.equal(radio.checked, true); + + input1.value = 'bar'; + await input1.dispatchEvent(event); + assert.equal(radio.checked, false); + + input2.value = 'bar'; + await input2.dispatchEvent(event); + assert.equal(radio.checked, true); + } +}; diff --git a/test/runtime/samples/binding-input-group-each-13/main.svelte b/test/runtime/samples/binding-input-group-each-13/main.svelte new file mode 100644 index 0000000000..592be2ae9d --- /dev/null +++ b/test/runtime/samples/binding-input-group-each-13/main.svelte @@ -0,0 +1,10 @@ + + + + + + + diff --git a/test/runtime/samples/binding-input-group-each-8/_config.js b/test/runtime/samples/binding-input-group-each-8/_config.js new file mode 100644 index 0000000000..b17b801a83 --- /dev/null +++ b/test/runtime/samples/binding-input-group-each-8/_config.js @@ -0,0 +1,78 @@ +// https://github.com/sveltejs/svelte/issues/7884 +export default { + async test({ assert, target, component, window }) { + let inputs = target.querySelectorAll('input'); + + assert.htmlEqual(target.innerHTML, ` +

{"foo":[],"bar":[]}

+

foo

+
    +
  • +
  • +
  • +
+

bar

+
    +
  • +
  • +
  • +
+ `); + + const event = new window.Event('change'); + + inputs[0].checked = true; + await inputs[0].dispatchEvent(event); + inputs[2].checked = true; + await inputs[2].dispatchEvent(event); + inputs[3].checked = true; + await inputs[3].dispatchEvent(event); + + assert.htmlEqual(target.innerHTML, ` +

{"foo":[1,3],"bar":[1]}

+

foo

+
    +
  • +
  • +
  • +
+

bar

+
    +
  • +
  • +
  • +
+ `); + + await component.update(); + + assert.htmlEqual(target.innerHTML, ` +

{"foo":[1,3],"bar":[1],"qux":[]}

+

qux

+
    +
  • +
  • +
  • +
+ `); + + inputs = target.querySelectorAll('input'); + inputs[0].checked = true; + await inputs[0].dispatchEvent(event); + + assert.htmlEqual(target.innerHTML, ` +

{"foo":[1,3],"bar":[1],"qux":[4]}

+

qux

+
    +
  • +
  • +
  • +
+ `); + assert.equal(inputs[0].checked, true); + assert.equal(inputs[1].checked, false); + assert.equal(inputs[2].checked, false); + } +}; + + diff --git a/test/runtime/samples/binding-input-group-each-8/main.svelte b/test/runtime/samples/binding-input-group-each-8/main.svelte new file mode 100644 index 0000000000..a6c5bccac0 --- /dev/null +++ b/test/runtime/samples/binding-input-group-each-8/main.svelte @@ -0,0 +1,36 @@ + + +

+ {JSON.stringify(object)} +

+ +{#each keys as key (key)} +

{key}

+
    + {#each values as value (value)} +
  • + +
  • + {/each} +
+{/each} diff --git a/test/runtime/samples/binding-input-group-each-9/_config.js b/test/runtime/samples/binding-input-group-each-9/_config.js new file mode 100644 index 0000000000..e2b48a83d7 --- /dev/null +++ b/test/runtime/samples/binding-input-group-each-9/_config.js @@ -0,0 +1,49 @@ +// https://github.com/sveltejs/svelte/issues/7633 +export default { + async test({ assert, target, component, window }) { + let inputs = target.querySelectorAll('input'); + + assert.equal(inputs[0].checked, true); + assert.equal(inputs[1].checked, false); + assert.equal(inputs[2].checked, false); + + await component.moveDown(0); + await component.moveDown(1); + + assert.htmlEqual( + target.innerHTML, + ` +
+ b +
+
+ c +
+
+ a +
+ ` + ); + + // after shifting order, should still keep the correct radio checked + inputs = target.querySelectorAll('input'); + assert.equal(inputs[0].checked, false); + assert.equal(inputs[1].checked, false); + assert.equal(inputs[2].checked, true); + + await (component.current = 'b'); + + inputs = target.querySelectorAll('input'); + assert.equal(inputs[0].checked, true); + assert.equal(inputs[1].checked, false); + assert.equal(inputs[2].checked, false); + + await component.moveDown(1); + + // after shifting order, should still keep the correct radio checked + inputs = target.querySelectorAll('input'); + assert.equal(inputs[0].checked, true); + assert.equal(inputs[1].checked, false); + assert.equal(inputs[2].checked, false); + } +}; diff --git a/test/runtime/samples/binding-input-group-each-9/main.svelte b/test/runtime/samples/binding-input-group-each-9/main.svelte new file mode 100644 index 0000000000..ee89123a9a --- /dev/null +++ b/test/runtime/samples/binding-input-group-each-9/main.svelte @@ -0,0 +1,36 @@ + + +{#each list as item} +
+ {item.name} + {#if true} + + {/if} +
+{/each} +