mirror of https://github.com/sveltejs/svelte
commit
8863459420
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
feat: add support for webkitdirectory DOM boolean attribute
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
fix: don't override instance methods during legacy class creation
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
feat: add reactive Map class to svelte/reactivity
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
fix: adjust scope parent for named slots
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
fix: improve handling of unowned derived signals
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
fix: improve element class attribute behaviour
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: add types for svelte/reactivity
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
fix: ensure select value is updated upon select option removal
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
fix: ensure capture events don't call delegated events
|
||||
@ -0,0 +1,5 @@
|
||||
---
|
||||
"svelte": patch
|
||||
---
|
||||
|
||||
fix: ensure arguments are supported on all reactive Date methods
|
||||
@ -1,2 +1,3 @@
|
||||
export { ReactiveDate as Date } from './date.js';
|
||||
export { ReactiveSet as Set } from './set.js';
|
||||
export { ReactiveMap as Map } from './map.js';
|
||||
|
||||
@ -0,0 +1,157 @@
|
||||
import { DEV } from 'esm-env';
|
||||
import { source, set } from '../internal/client/reactivity/sources.js';
|
||||
import { get } from '../internal/client/runtime.js';
|
||||
import { UNINITIALIZED } from '../internal/client/constants.js';
|
||||
import { map } from './utils.js';
|
||||
|
||||
/**
|
||||
* @template K
|
||||
* @template V
|
||||
*/
|
||||
export class ReactiveMap extends Map {
|
||||
/** @type {Map<K, import('#client').Source<V>>} */
|
||||
#sources = new Map();
|
||||
#version = source(0);
|
||||
#size = source(0);
|
||||
|
||||
/**
|
||||
* @param {Iterable<readonly [K, V]> | null | undefined} [value]
|
||||
*/
|
||||
constructor(value) {
|
||||
super();
|
||||
|
||||
// If the value is invalid then the native exception will fire here
|
||||
if (DEV) new Map(value);
|
||||
|
||||
if (value) {
|
||||
var sources = this.#sources;
|
||||
|
||||
for (var [key, v] of value) {
|
||||
sources.set(key, source(v));
|
||||
super.set(key, v);
|
||||
}
|
||||
|
||||
this.#size.v = sources.size;
|
||||
}
|
||||
}
|
||||
|
||||
#increment_version() {
|
||||
set(this.#version, this.#version.v + 1);
|
||||
}
|
||||
|
||||
/** @param {K} key */
|
||||
has(key) {
|
||||
var s = this.#sources.get(key);
|
||||
|
||||
if (s === undefined) {
|
||||
// We should always track the version in case
|
||||
// the Set ever gets this value in the future.
|
||||
get(this.#version);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
get(s);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(value: V, key: K, map: Map<K, V>) => void} callbackfn
|
||||
* @param {any} [this_arg]
|
||||
*/
|
||||
forEach(callbackfn, this_arg) {
|
||||
get(this.#version);
|
||||
|
||||
return super.forEach(callbackfn, this_arg);
|
||||
}
|
||||
|
||||
/** @param {K} key */
|
||||
get(key) {
|
||||
var s = this.#sources.get(key);
|
||||
|
||||
if (s === undefined) {
|
||||
// We should always track the version in case
|
||||
// the Set ever gets this value in the future.
|
||||
get(this.#version);
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return get(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {K} key
|
||||
* @param {V} value
|
||||
* */
|
||||
set(key, value) {
|
||||
var sources = this.#sources;
|
||||
var s = sources.get(key);
|
||||
|
||||
if (s === undefined) {
|
||||
sources.set(key, source(value));
|
||||
set(this.#size, sources.size);
|
||||
this.#increment_version();
|
||||
} else {
|
||||
set(s, value);
|
||||
}
|
||||
|
||||
return super.set(key, value);
|
||||
}
|
||||
|
||||
/** @param {K} key */
|
||||
delete(key) {
|
||||
var sources = this.#sources;
|
||||
var s = sources.get(key);
|
||||
|
||||
if (s !== undefined) {
|
||||
sources.delete(key);
|
||||
set(this.#size, sources.size);
|
||||
set(s, /** @type {V} */ (UNINITIALIZED));
|
||||
this.#increment_version();
|
||||
}
|
||||
|
||||
return super.delete(key);
|
||||
}
|
||||
|
||||
clear() {
|
||||
var sources = this.#sources;
|
||||
|
||||
if (sources.size !== 0) {
|
||||
set(this.#size, 0);
|
||||
for (var s of sources.values()) {
|
||||
set(s, /** @type {V} */ (UNINITIALIZED));
|
||||
}
|
||||
this.#increment_version();
|
||||
}
|
||||
|
||||
sources.clear();
|
||||
super.clear();
|
||||
}
|
||||
|
||||
keys() {
|
||||
get(this.#version);
|
||||
return this.#sources.keys();
|
||||
}
|
||||
|
||||
values() {
|
||||
get(this.#version);
|
||||
return map(this.#sources.values(), get);
|
||||
}
|
||||
|
||||
entries() {
|
||||
get(this.#version);
|
||||
return map(
|
||||
this.#sources.entries(),
|
||||
([key, source]) => /** @type {[K, V]} */ ([key, get(source)])
|
||||
);
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this.entries();
|
||||
}
|
||||
|
||||
get size() {
|
||||
return get(this.#size);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,151 @@
|
||||
import { pre_effect, user_root_effect } from '../internal/client/reactivity/effects.js';
|
||||
import { flushSync } from '../main/main-client.js';
|
||||
import { ReactiveMap } from './map.js';
|
||||
import { assert, test } from 'vitest';
|
||||
|
||||
test('map.values()', () => {
|
||||
const map = new ReactiveMap([
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 3],
|
||||
[4, 4],
|
||||
[5, 5]
|
||||
]);
|
||||
|
||||
const log: any = [];
|
||||
|
||||
const cleanup = user_root_effect(() => {
|
||||
pre_effect(() => {
|
||||
log.push(map.size);
|
||||
});
|
||||
|
||||
pre_effect(() => {
|
||||
log.push(map.has(3));
|
||||
});
|
||||
|
||||
pre_effect(() => {
|
||||
log.push(Array.from(map.values()));
|
||||
});
|
||||
});
|
||||
|
||||
flushSync(() => {
|
||||
map.delete(3);
|
||||
});
|
||||
|
||||
flushSync(() => {
|
||||
map.clear();
|
||||
});
|
||||
|
||||
assert.deepEqual(log, [5, true, [1, 2, 3, 4, 5], 4, false, [1, 2, 4, 5], 0, [], false]); // TODO update when we fix effect ordering bug
|
||||
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test('map.get(...)', () => {
|
||||
const map = new ReactiveMap([
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 3]
|
||||
]);
|
||||
|
||||
const log: any = [];
|
||||
|
||||
const cleanup = user_root_effect(() => {
|
||||
pre_effect(() => {
|
||||
log.push('get 1', map.get(1));
|
||||
});
|
||||
|
||||
pre_effect(() => {
|
||||
log.push('get 2', map.get(2));
|
||||
});
|
||||
|
||||
pre_effect(() => {
|
||||
log.push('get 3', map.get(3));
|
||||
});
|
||||
});
|
||||
|
||||
flushSync(() => {
|
||||
map.delete(2);
|
||||
});
|
||||
|
||||
flushSync(() => {
|
||||
map.set(2, 2);
|
||||
});
|
||||
|
||||
assert.deepEqual(log, ['get 1', 1, 'get 2', 2, 'get 3', 3, 'get 2', undefined, 'get 2', 2]);
|
||||
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test('map.has(...)', () => {
|
||||
const map = new ReactiveMap([
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
[3, 3]
|
||||
]);
|
||||
|
||||
const log: any = [];
|
||||
|
||||
const cleanup = user_root_effect(() => {
|
||||
pre_effect(() => {
|
||||
log.push('has 1', map.has(1));
|
||||
});
|
||||
|
||||
pre_effect(() => {
|
||||
log.push('has 2', map.has(2));
|
||||
});
|
||||
|
||||
pre_effect(() => {
|
||||
log.push('has 3', map.has(3));
|
||||
});
|
||||
});
|
||||
|
||||
flushSync(() => {
|
||||
map.delete(2);
|
||||
});
|
||||
|
||||
flushSync(() => {
|
||||
map.set(2, 2);
|
||||
});
|
||||
|
||||
assert.deepEqual(log, [
|
||||
'has 1',
|
||||
true,
|
||||
'has 2',
|
||||
true,
|
||||
'has 3',
|
||||
true,
|
||||
'has 2',
|
||||
false,
|
||||
'has 2',
|
||||
true
|
||||
]);
|
||||
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test('map handling of undefined values', () => {
|
||||
const map = new ReactiveMap();
|
||||
|
||||
const log: any = [];
|
||||
|
||||
const cleanup = user_root_effect(() => {
|
||||
map.set(1, undefined);
|
||||
|
||||
pre_effect(() => {
|
||||
log.push(map.get(1));
|
||||
});
|
||||
|
||||
flushSync(() => {
|
||||
map.delete(1);
|
||||
});
|
||||
|
||||
flushSync(() => {
|
||||
map.set(1, 1);
|
||||
});
|
||||
});
|
||||
|
||||
assert.deepEqual(log, [undefined, undefined, 1]);
|
||||
|
||||
cleanup();
|
||||
});
|
||||
@ -0,0 +1,24 @@
|
||||
/**
|
||||
* @template T
|
||||
* @template U
|
||||
* @param {Iterable<T>} iterable
|
||||
* @param {(value: T) => U} fn
|
||||
* @returns {IterableIterator<U>}
|
||||
*/
|
||||
export function map(iterable, fn) {
|
||||
return {
|
||||
[Symbol.iterator]: get_this,
|
||||
next() {
|
||||
for (const value of iterable) {
|
||||
return { done: false, value: fn(value) };
|
||||
}
|
||||
|
||||
return { done: true, value: undefined };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** @this {any} */
|
||||
function get_this() {
|
||||
return this;
|
||||
}
|
||||
@ -1 +1 @@
|
||||
<!--ssr:0--><div class="foo"></div><!--ssr:0-->
|
||||
<!--ssr:0--><div id="foo"></div><!--ssr:0-->
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script>
|
||||
export let className;
|
||||
export let id;
|
||||
</script>
|
||||
|
||||
<div class={className}></div>
|
||||
<div id={id}></div>
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
await new Promise((r) => setTimeout(r, 200)); // wait for await block to resolve
|
||||
|
||||
const options = target.querySelectorAll('option');
|
||||
assert.ok(!options[0].selected);
|
||||
assert.ok(options[1].selected);
|
||||
assert.ok(!options[2].selected);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,19 @@
|
||||
<script>
|
||||
let promise = getNumbers();
|
||||
let selected = 2;
|
||||
|
||||
async function getNumbers() {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
return [1, 2, 3];
|
||||
}
|
||||
</script>
|
||||
|
||||
<select bind:value={selected}>
|
||||
{#await promise}
|
||||
<option>-1</option>
|
||||
{:then numbers}
|
||||
{#each numbers as number}
|
||||
<option>{number}</option>
|
||||
{/each}
|
||||
{/await}
|
||||
</select>
|
||||
@ -0,0 +1,35 @@
|
||||
import { ok, test } from '../../test';
|
||||
|
||||
// test select binding behavior when a selected option is removed
|
||||
export default test({
|
||||
skip_if_ssr: 'permanent',
|
||||
|
||||
html: `<p>selected: a</p><select><option value="a">a</option><option value="b">b</option><option value="c">c</option></select>`,
|
||||
|
||||
async test({ assert, component, target }) {
|
||||
const select = target.querySelector('select');
|
||||
ok(select);
|
||||
const options = target.querySelectorAll('option');
|
||||
|
||||
// first option should be selected by default since no value was bound
|
||||
assert.equal(component.selected, 'a');
|
||||
assert.equal(select.value, 'a');
|
||||
assert.ok(options[0].selected);
|
||||
|
||||
// remove the selected item, so the bound value no longer matches anything
|
||||
component.items = ['b', 'c'];
|
||||
|
||||
// There's a MutationObserver
|
||||
await Promise.resolve();
|
||||
|
||||
// now no option should be selected
|
||||
assert.equal(select.value, '');
|
||||
assert.equal(select.selectedIndex, -1);
|
||||
|
||||
// model of selected value should be kept around, even if it is not in the list
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`<p>selected: a</p><select><option value="b">b</option><option value="c">c</option></select>`
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,12 @@
|
||||
<script>
|
||||
export let selected;
|
||||
export let items = ['a', 'b', 'c'];
|
||||
</script>
|
||||
|
||||
<p>selected: {selected}</p>
|
||||
|
||||
<select bind:value={selected}>
|
||||
{#each items as letter}
|
||||
<option>{letter}</option>
|
||||
{/each}
|
||||
</select>
|
||||
@ -0,0 +1 @@
|
||||
<slot name="inner" text="hello world" />
|
||||
@ -0,0 +1,9 @@
|
||||
<script>
|
||||
export let text;
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{text}
|
||||
<hr />
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
@ -0,0 +1,7 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
html: `
|
||||
<div>hello world <hr> <div slot="footer">hello world</div></div>
|
||||
`
|
||||
});
|
||||
@ -0,0 +1,12 @@
|
||||
<script>
|
||||
import Nested from "./Nested.svelte"
|
||||
import Nested2 from "./Nested2.svelte"
|
||||
</script>
|
||||
|
||||
<Nested>
|
||||
<Nested2 slot="inner" let:text {text}>
|
||||
<div slot="footer">
|
||||
{text}
|
||||
</div>
|
||||
</Nested2>
|
||||
</Nested>
|
||||
@ -0,0 +1,16 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
await target.querySelector('button')?.click();
|
||||
await Promise.resolve();
|
||||
|
||||
const options = target.querySelectorAll('option');
|
||||
assert.equal(options[0].selected, false);
|
||||
assert.equal(options[1].selected, true);
|
||||
assert.equal(options[2].selected, false);
|
||||
assert.equal(options[3].selected, false);
|
||||
assert.equal(options[4].selected, true);
|
||||
assert.equal(options[5].selected, false);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,33 @@
|
||||
<script>
|
||||
let value = 'bar';
|
||||
let value_bound = 'bar';
|
||||
let options = {};
|
||||
|
||||
function loadOptions() {
|
||||
options = {
|
||||
foo: 'Foo',
|
||||
bar: 'Bar',
|
||||
baz: 'Baz',
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<select {value}>
|
||||
{#each Object.entries(options) as [key, value] (key)}
|
||||
<option value={key}>
|
||||
{value}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<select bind:value={value_bound}>
|
||||
{#each Object.entries(options) as [key, value] (key)}
|
||||
<option value={key}>
|
||||
{value}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
|
||||
<button on:click={loadOptions}>
|
||||
Load options
|
||||
</button>
|
||||
@ -0,0 +1,21 @@
|
||||
import { test } from '../../test';
|
||||
|
||||
const items = [{ id: 'a' }, { id: 'b' }];
|
||||
|
||||
export default test({
|
||||
get props() {
|
||||
return { foo: [items[0]], items };
|
||||
},
|
||||
|
||||
test({ assert, component, target }) {
|
||||
const options = target.querySelectorAll('option');
|
||||
|
||||
assert.equal(options[0].selected, true);
|
||||
assert.equal(options[1].selected, false);
|
||||
|
||||
component.foo = [component.items[1]];
|
||||
|
||||
assert.equal(options[0].selected, false);
|
||||
assert.equal(options[1].selected, true);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,9 @@
|
||||
<script>
|
||||
export let foo;
|
||||
export let items;
|
||||
</script>
|
||||
|
||||
<select value="{foo}" multiple>
|
||||
<option value='{items[0]}'>a</option>
|
||||
<option value='{items[1]}'>b</option>
|
||||
</select>
|
||||
@ -0,0 +1,30 @@
|
||||
import { flushSync } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
async test({ assert, target }) {
|
||||
let [btn1, btn2] = target.querySelectorAll('button');
|
||||
|
||||
flushSync(() => {
|
||||
btn1.click();
|
||||
});
|
||||
|
||||
flushSync(() => {
|
||||
btn2.click();
|
||||
});
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`<button>Activate</button><button>Toggle</button>\nneighba\nneighba`
|
||||
);
|
||||
|
||||
flushSync(() => {
|
||||
btn2.click();
|
||||
});
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`<button>Activate</button><button>Toggle</button>\nzeeba\nzeeba`
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,49 @@
|
||||
<script>
|
||||
import { untrack } from 'svelte';
|
||||
|
||||
class Model {
|
||||
data = $state();
|
||||
|
||||
constructor(data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
name = $derived(this.data?.name);
|
||||
source = $derived(this.data?.source);
|
||||
|
||||
toggle() {
|
||||
this.data.name = this.data.name === 'zeeba' ? 'neighba' : 'zeeba';
|
||||
}
|
||||
}
|
||||
|
||||
let model = $state(new Model({ name: 'zeeba', source: 'initial' }));
|
||||
|
||||
let setModel = (source) => {
|
||||
let next = new Model({ name: 'zeeba', source });
|
||||
model = next;
|
||||
}
|
||||
|
||||
let needsSet = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if(needsSet) {
|
||||
setModel('effect');
|
||||
untrack(() => {
|
||||
needsSet = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let setWithEffect = () => {
|
||||
needsSet = true;
|
||||
};
|
||||
|
||||
let toggle = () => {
|
||||
model.toggle();
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={setWithEffect}>Activate</button>
|
||||
<button onclick={toggle}>Toggle</button>
|
||||
{model.name}
|
||||
{model.data.name}
|
||||
@ -0,0 +1,16 @@
|
||||
import { test } from '../../test';
|
||||
import { log } from './log.js';
|
||||
|
||||
export default test({
|
||||
before_test() {
|
||||
log.length = 0;
|
||||
},
|
||||
|
||||
async test({ assert, target }) {
|
||||
const btn = target.querySelector('button');
|
||||
|
||||
btn?.click();
|
||||
await Promise.resolve();
|
||||
assert.deepEqual(log, ['div onclickcapture', 'button onclick']);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,2 @@
|
||||
/** @type {any[]} */
|
||||
export const log = [];
|
||||
@ -0,0 +1,7 @@
|
||||
<script>
|
||||
import { log } from "./log";
|
||||
</script>
|
||||
|
||||
<div onclickcapture={() => log.push('div onclickcapture')}>
|
||||
<button onclick={() => log.push('button onclick')}>main</button>
|
||||
</div>
|
||||
@ -0,0 +1,50 @@
|
||||
import { flushSync } from '../../../../src/main/main-client';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
html: `<button>add</button><button>delete</button><button>clear</button>`,
|
||||
|
||||
test({ assert, target }) {
|
||||
const [btn, btn2, btn3] = target.querySelectorAll('button');
|
||||
|
||||
flushSync(() => {
|
||||
btn?.click();
|
||||
});
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`<button>add</button><button>delete</button><button>clear</button><div>1:1</div>`
|
||||
);
|
||||
|
||||
flushSync(() => {
|
||||
btn?.click();
|
||||
});
|
||||
|
||||
flushSync(() => {
|
||||
btn?.click();
|
||||
});
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`<button>add</button><button>delete</button><button>clear</button><div>1:1</div><div>2:2</div><div>3:3</div>`
|
||||
);
|
||||
|
||||
flushSync(() => {
|
||||
btn2?.click();
|
||||
});
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`<button>add</button><button>delete</button><button>clear</button><div>1:1</div><div>2:2</div>`
|
||||
);
|
||||
|
||||
flushSync(() => {
|
||||
btn3?.click();
|
||||
});
|
||||
|
||||
assert.htmlEqual(
|
||||
target.innerHTML,
|
||||
`<button>add</button><button>delete</button><button>clear</button>`
|
||||
);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,21 @@
|
||||
<script>
|
||||
import {Map} from 'svelte/reactivity';
|
||||
|
||||
let state = new Map();
|
||||
</script>
|
||||
|
||||
<button onclick={() => {
|
||||
state.set(state.size + 1, state.size + 1);
|
||||
}}>add</button>
|
||||
|
||||
<button onclick={() => {
|
||||
state.delete(state.size);
|
||||
}}>delete</button>
|
||||
|
||||
<button onclick={() => {
|
||||
state.clear();
|
||||
}}>clear</button>
|
||||
|
||||
{#each state as [key, value]}
|
||||
<div>{key}:{value}</div>
|
||||
{/each}
|
||||
Loading…
Reference in new issue