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
pull/8349/head
Tan Li Hau 2 years ago committed by GitHub
parent 6476e9b34f
commit 0966d1d282
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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<string, Bindings>;
binding_group_initialised: Set<string> = new Set();
binding_groups: Set<BindingGroup> = new Set();
chunks: {
declarations: Array<Node | Node[]>;
@ -249,6 +250,7 @@ export default class Block {
}
}
this.render_binding_groups();
this.render_listeners();
const properties: Record<string, any> = {};
@ -500,4 +502,10 @@ export default class Block {
}
}
}
render_binding_groups() {
for (const binding_group of this.binding_groups) {
binding_group.render();
}
}
}

@ -22,6 +22,15 @@ type BitMasks = Array<{
names: string[];
}>;
export interface BindingGroup {
binding_group: (to_reference?: boolean) => Node;
contexts: string[];
list_dependencies: Set<string>;
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<Block | Node | Node[]> = [];
readonly: Set<string> = new Set();
meta_bindings: Array<Node | Node[]> = []; // initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
binding_groups: Map<string, { binding_group: (to_reference?: boolean) => Node; is_context: boolean; contexts: string[]; index: number; keypath: string }> = new Map();
binding_groups: Map<string, BindingGroup> = 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,

@ -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));

@ -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<string>();
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<string>
) {
const { node } = element;
@ -427,7 +451,7 @@ function get_value_from_dom(
// <input type='checkbox' bind:group='foo'>
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)`;
}

@ -163,6 +163,8 @@ export default class ElementWrapper extends Wrapper {
has_dynamic_attribute: boolean;
select_binding_dependencies?: Set<string>;
has_dynamic_value: boolean;
dynamic_value_condition: any;
var: any;
void: boolean;

@ -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<NodeEx>,
};
@ -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 <head>
let children: ArrayLike<NodeEx2> = target.childNodes as NodeListOf<NodeEx2>;
@ -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<T=any>(type: string, detail?: T, { bubbles = false, cancelable = false } = {}): CustomEvent<T> {
export function custom_event<T = any>(type: string, detail?: T, { bubbles = false, cancelable = false } = {}): CustomEvent<T> {
const e: CustomEvent<T> = document.createEvent('CustomEvent');
e.initCustomEvent(type, bubbles, cancelable, detail);
return e;

@ -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;
}

@ -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,
`
<div class="item">
b <label><input name="current" type="radio" value="b"> current</label>
</div>
<div class="item">
c <label><input name="current" type="radio" value="c"> current</label>
</div>
<div class="item">
a <label><input name="current" type="radio" value="a"> current</label>
</div>
`
);
// 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);
}
};

@ -0,0 +1,36 @@
<script>
export let list = [
{ name: "a", text: "This is a test." },
{ name: "b", text: "This is another test." },
{ name: "c", text: "This is also a test." },
];
export let current = "a";
export function moveUp(i) {
list = [
...list.slice(0, Math.max(i - 1, 0)),
list[i],
list[i - 1],
...list.slice(i + 1),
];
}
export function moveDown(i) {
moveUp(i + 1);
}
</script>
{#each list as item (item.name)}
<div class="item">
{item.name}
{#if true}
<label
><input
type="radio"
name="current"
bind:group={current}
value={item.name}
/> current</label
>
{/if}
</div>
{/each}

@ -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,
`
<div>1</div>
<div>2
<div class="arg">
<input type="radio" value="a">
<input type="radio" value="b">
</div>
<div class="arg">
<input type="radio" value="c">
<input type="radio" value="d">
</div>
</div>
<div>3
<div class="arg">
<input type="radio" value="a">
<input type="radio" value="b">
</div>
<div class="arg">
<input type="radio" value="c">
<input type="radio" value="d">
</div>
</div>
`
);
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,
`
<div>2
<div class="arg">
<input type="radio" value="a">
<input type="radio" value="b">
</div>
<div class="arg">
<input type="radio" value="c">
<input type="radio" value="d">
</div>
</div>
<div>3
<div class="arg">
<input type="radio" value="a">
<input type="radio" value="b">
</div>
<div class="arg">
<input type="radio" value="c">
<input type="radio" value="d">
</div>
</div>
`
);
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]));
}
};

@ -0,0 +1,60 @@
<script>
export let pipelineOperations = [
{
operation: {
name: "foo",
args: [],
},
id: 1,
},
{
operation: {
name: "bar",
args: [
{
name: "bar_1",
value: "a",
options: [{ value: "a" }, { value: "b" }],
},
{
name: "bar_2",
value: "c",
options: [{ value: "c" }, { value: "d" }],
},
],
},
id: 2,
},
{
operation: {
name: "baz",
args: [
{
name: "baz_1",
value: "b",
options: [{ value: "a" }, { value: "b" }],
},
{
name: "baz_2",
value: "c",
options: [{ value: "c" }, { value: "d" }],
},
],
},
id: 3,
},
];
</script>
{#each pipelineOperations as { operation, id } (id)}
<div>
{id}
{#each operation.args as arg}
<div class="arg">
{#each arg.options as { value }}
<input type="radio" bind:group={arg.value} {value} />
{/each}
</div>
{/each}
</div>
{/each}

@ -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,
`
<div>1</div>
<div>2
<div class="arg">
<input type="checkbox" value="a">
<input type="checkbox" value="b">
</div>
<div class="arg">
<input type="checkbox" value="c">
<input type="checkbox" value="d">
</div>
</div>
<div>3
<div class="arg">
<input type="checkbox" value="a">
<input type="checkbox" value="b">
</div>
<div class="arg">
<input type="checkbox" value="c">
<input type="checkbox" value="d">
</div>
</div>
`
);
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,
`
<div>2
<div class="arg">
<input type="checkbox" value="a">
<input type="checkbox" value="b">
</div>
<div class="arg">
<input type="checkbox" value="c">
<input type="checkbox" value="d">
</div>
</div>
<div>3
<div class="arg">
<input type="checkbox" value="a">
<input type="checkbox" value="b">
</div>
<div class="arg">
<input type="checkbox" value="c">
<input type="checkbox" value="d">
</div>
</div>
`
);
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']);
}
};

@ -0,0 +1,60 @@
<script>
export let pipelineOperations = [
{
operation: {
name: "foo",
args: [],
},
id: 1,
},
{
operation: {
name: "bar",
args: [
{
name: "bar_1",
value: ["a"],
options: [{ value: "a" }, { value: "b" }],
},
{
name: "bar_2",
value: ["c"],
options: [{ value: "c" }, { value: "d" }],
},
],
},
id: 2,
},
{
operation: {
name: "baz",
args: [
{
name: "baz_1",
value: [],
options: [{ value: "a" }, { value: "b" }],
},
{
name: "baz_2",
value: [],
options: [{ value: "c" }, { value: "d" }],
},
],
},
id: 3,
},
];
</script>
{#each pipelineOperations as { operation, id } (id)}
<div>
{id}
{#each operation.args as arg}
<div class="arg">
{#each arg.options as { value }}
<input type="checkbox" bind:group={arg.value} {value} />
{/each}
</div>
{/each}
</div>
{/each}

@ -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);
}
};

@ -0,0 +1,10 @@
<script>
let name = 'world';
let current = '';
</script>
<input type="radio" name="current" bind:group={current} value={name}>
<input type="text" bind:value={current} />
<input type="text" bind:value={name} />

@ -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, `
<p>{"foo":[],"bar":[]}</p>
<h2>foo</h2>
<ul>
<li><label><input name="foo" type="checkbox" value="1"> 1</label></li>
<li><label><input name="foo" type="checkbox" value="2"> 2</label></li>
<li><label><input name="foo" type="checkbox" value="3"> 3</label></li>
</ul>
<h2>bar</h2>
<ul>
<li><label><input name="bar" type="checkbox" value="1"> 1</label></li>
<li><label><input name="bar" type="checkbox" value="2"> 2</label></li>
<li><label><input name="bar" type="checkbox" value="3"> 3</label></li>
</ul>
`);
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, `
<p>{"foo":[1,3],"bar":[1]}</p>
<h2>foo</h2>
<ul>
<li><label><input name="foo" type="checkbox" value="1"> 1</label></li>
<li><label><input name="foo" type="checkbox" value="2"> 2</label></li>
<li><label><input name="foo" type="checkbox" value="3"> 3</label></li>
</ul>
<h2>bar</h2>
<ul>
<li><label><input name="bar" type="checkbox" value="1"> 1</label></li>
<li><label><input name="bar" type="checkbox" value="2"> 2</label></li>
<li><label><input name="bar" type="checkbox" value="3"> 3</label></li>
</ul>
`);
await component.update();
assert.htmlEqual(target.innerHTML, `
<p>{"foo":[1,3],"bar":[1],"qux":[]}</p>
<h2>qux</h2>
<ul>
<li><label><input name="qux" type="checkbox" value="4"> 4</label></li>
<li><label><input name="qux" type="checkbox" value="5"> 5</label></li>
<li><label><input name="qux" type="checkbox" value="6"> 6</label></li>
</ul>
`);
inputs = target.querySelectorAll('input');
inputs[0].checked = true;
await inputs[0].dispatchEvent(event);
assert.htmlEqual(target.innerHTML, `
<p>{"foo":[1,3],"bar":[1],"qux":[4]}</p>
<h2>qux</h2>
<ul>
<li><label><input name="qux" type="checkbox" value="4"> 4</label></li>
<li><label><input name="qux" type="checkbox" value="5"> 5</label></li>
<li><label><input name="qux" type="checkbox" value="6"> 6</label></li>
</ul>
`);
assert.equal(inputs[0].checked, true);
assert.equal(inputs[1].checked, false);
assert.equal(inputs[2].checked, false);
}
};

@ -0,0 +1,36 @@
<script>
let keys = ["foo", "bar"];
let values = [1, 2, 3];
const object = {};
$: keys.forEach((key) => {
// Make sure Svelte has an array to bind to
if (!object[key]) {
object[key] = [];
}
});
export function update() {
keys = ["qux"];
values = [4, 5, 6];
}
</script>
<p>
{JSON.stringify(object)}
</p>
{#each keys as key (key)}
<h2>{key}</h2>
<ul>
{#each values as value (value)}
<li>
<label>
<input type="checkbox" name={key} {value} bind:group={object[key]} />
{value}
</label>
</li>
{/each}
</ul>
{/each}

@ -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,
`
<div class="item">
b <label><input name="current" type="radio" value="b"> current</label>
</div>
<div class="item">
c <label><input name="current" type="radio" value="c"> current</label>
</div>
<div class="item">
a <label><input name="current" type="radio" value="a"> current</label>
</div>
`
);
// 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);
}
};

@ -0,0 +1,36 @@
<script>
export let list = [
{ name: "a", text: "This is a test." },
{ name: "b", text: "This is another test." },
{ name: "c", text: "This is also a test." },
];
export let current = "a";
export function moveUp(i) {
list = [
...list.slice(0, Math.max(i - 1, 0)),
list[i],
list[i - 1],
...list.slice(i + 1),
];
}
export function moveDown(i) {
moveUp(i + 1);
}
</script>
{#each list as item}
<div class="item">
{item.name}
{#if true}
<label
><input
type="radio"
name="current"
bind:group={current}
value={item.name}
/> current</label
>
{/if}
</div>
{/each}
Loading…
Cancel
Save