Merge branch 'main' into props-bindable

props-bindable
Simon Holthausen 1 year ago
commit e17e5e78bc

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: add reactive Map class to svelte/reactivity

@ -98,6 +98,7 @@
"friendly-candles-relate", "friendly-candles-relate",
"friendly-lies-camp", "friendly-lies-camp",
"funny-wombats-argue", "funny-wombats-argue",
"fuzzy-bags-camp",
"gentle-dolls-juggle", "gentle-dolls-juggle",
"gentle-sheep-hug", "gentle-sheep-hug",
"gentle-spies-happen", "gentle-spies-happen",
@ -234,6 +235,7 @@
"selfish-dragons-knock", "selfish-dragons-knock",
"selfish-tools-hide", "selfish-tools-hide",
"serious-kids-deliver", "serious-kids-deliver",
"serious-needles-joke",
"serious-socks-cover", "serious-socks-cover",
"serious-zebras-scream", "serious-zebras-scream",
"seven-deers-jam", "seven-deers-jam",
@ -344,6 +346,7 @@
"wild-foxes-wonder", "wild-foxes-wonder",
"wise-apples-care", "wise-apples-care",
"wise-dancers-hang", "wise-dancers-hang",
"wise-dodos-tell",
"wise-donkeys-marry", "wise-donkeys-marry",
"wise-jobs-admire", "wise-jobs-admire",
"wise-radios-exercise", "wise-radios-exercise",

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: add types for svelte/reactivity

@ -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,5 +1,19 @@
# svelte # svelte
## 5.0.0-next.80
### Patch Changes
- fix: add types for svelte/reactivity ([#10817](https://github.com/sveltejs/svelte/pull/10817))
- fix: ensure arguments are supported on all reactive Date methods ([#10813](https://github.com/sveltejs/svelte/pull/10813))
## 5.0.0-next.79
### Patch Changes
- feat: add reactive Map class to svelte/reactivity ([#10803](https://github.com/sveltejs/svelte/pull/10803))
## 5.0.0-next.78 ## 5.0.0-next.78
### Patch Changes ### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.0.0-next.78", "version": "5.0.0-next.80",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {

@ -29,6 +29,7 @@ await createBundle({
[`${pkg.name}/easing`]: `${dir}/src/easing/index.js`, [`${pkg.name}/easing`]: `${dir}/src/easing/index.js`,
[`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`, [`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`,
[`${pkg.name}/motion`]: `${dir}/src/motion/public.d.ts`, [`${pkg.name}/motion`]: `${dir}/src/motion/public.d.ts`,
[`${pkg.name}/reactivity`]: `${dir}/src/reactivity/index.js`,
[`${pkg.name}/server`]: `${dir}/src/server/index.js`, [`${pkg.name}/server`]: `${dir}/src/server/index.js`,
[`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`, [`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`,
[`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`, [`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`,

@ -17,7 +17,10 @@ export function event(event_name, dom, handler, capture, passive) {
* @this {EventTarget} * @this {EventTarget}
*/ */
function target_handler(/** @type {Event} */ event) { function target_handler(/** @type {Event} */ event) {
handle_event_propagation(dom, event); if (!capture) {
// Only call in the bubble phase, else delegated events would be called before the capturing events
handle_event_propagation(dom, event);
}
if (!event.cancelBubble) { if (!event.cancelBubble) {
return handler.call(this, event); return handler.call(this, event);
} }

@ -48,7 +48,6 @@ export function proxy(value, immutable = true, owners) {
const prototype = get_prototype_of(value); const prototype = get_prototype_of(value);
// TODO handle Map and Set as well
if (prototype === object_prototype || prototype === array_prototype) { if (prototype === object_prototype || prototype === array_prototype) {
const proxy = new Proxy(value, state_proxy_handler); const proxy = new Proxy(value, state_proxy_handler);

@ -55,29 +55,30 @@ const write = [
'setYear' 'setYear'
]; ];
var inited = false;
export class ReactiveDate extends Date { export class ReactiveDate extends Date {
#raw_time = source(super.getTime()); #raw_time = source(super.getTime());
static #inited = false;
// We init as part of the first instance so that we can treeshake this class // We init as part of the first instance so that we can treeshake this class
#init() { #init() {
if (!ReactiveDate.#inited) { if (!inited) {
ReactiveDate.#inited = true; inited = true;
const proto = ReactiveDate.prototype; const proto = ReactiveDate.prototype;
const date_proto = Date.prototype; const date_proto = Date.prototype;
for (const method of read) { for (const method of read) {
// @ts-ignore // @ts-ignore
proto[method] = function () { proto[method] = function (...args) {
get(this.#raw_time); get(this.#raw_time);
// @ts-ignore // @ts-ignore
return date_proto[method].call(this); return date_proto[method].apply(this, args);
}; };
} }
for (const method of write) { for (const method of write) {
// @ts-ignore // @ts-ignore
proto[method] = function (/** @type {any} */ ...args) { proto[method] = function (...args) {
// @ts-ignore // @ts-ignore
const v = date_proto[method].apply(this, args); const v = date_proto[method].apply(this, args);
const time = date_proto.getTime.call(this); const time = date_proto.getTime.call(this);

@ -1,2 +1,3 @@
export { ReactiveDate as Date } from './date.js'; export { ReactiveDate as Date } from './date.js';
export { ReactiveSet as Set } from './set.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();
});

@ -1,33 +1,10 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { source, set } from '../internal/client/reactivity/sources.js'; import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js'; import { get } from '../internal/client/runtime.js';
import { map } from './utils.js';
var read = [ var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf'];
'difference', var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'union'];
'forEach',
'intersection',
'isDisjointFrom',
'isSubsetOf',
'isSupersetOf',
'symmetricDifference',
'union'
];
/**
* @template T
* @param {IterableIterator<T>} iterator
*/
function make_iterable(iterator) {
iterator[Symbol.iterator] = get_self;
return iterator;
}
/**
* @this {any}
*/
function get_self() {
return this;
}
var inited = false; var inited = false;
@ -41,7 +18,7 @@ export class ReactiveSet extends Set {
#size = source(0); #size = source(0);
/** /**
* @param {Iterable<T> | null | undefined} value * @param {Iterable<T> | null | undefined} [value]
*/ */
constructor(value) { constructor(value) {
super(); super();
@ -50,9 +27,14 @@ export class ReactiveSet extends Set {
if (DEV) new Set(value); if (DEV) new Set(value);
if (value) { if (value) {
var sources = this.#sources;
for (var element of value) { for (var element of value) {
this.add(element); sources.set(element, source(true));
super.add(element);
} }
this.#size.v = sources.size;
} }
if (!inited) this.#init(); if (!inited) this.#init();
@ -65,7 +47,10 @@ export class ReactiveSet extends Set {
var proto = ReactiveSet.prototype; var proto = ReactiveSet.prototype;
var set_proto = Set.prototype; var set_proto = Set.prototype;
for (var method of read) { /** @type {string} */
var method;
for (method of read_methods) {
// @ts-ignore // @ts-ignore
proto[method] = function (...v) { proto[method] = function (...v) {
get(this.#version); get(this.#version);
@ -73,6 +58,17 @@ export class ReactiveSet extends Set {
return set_proto[method].apply(this, v); return set_proto[method].apply(this, v);
}; };
} }
for (method of set_like_methods) {
// @ts-ignore
proto[method] = function (...v) {
get(this.#version);
// @ts-ignore
var set = /** @type {Set<T>} */ (set_proto[method].apply(this, v));
return new ReactiveSet(set);
};
}
} }
#increment_version() { #increment_version() {
@ -81,9 +77,9 @@ export class ReactiveSet extends Set {
/** @param {T} value */ /** @param {T} value */
has(value) { has(value) {
var source = this.#sources.get(value); var s = this.#sources.get(value);
if (source === undefined) { if (s === undefined) {
// We should always track the version in case // We should always track the version in case
// the Set ever gets this value in the future. // the Set ever gets this value in the future.
get(this.#version); get(this.#version);
@ -91,7 +87,7 @@ export class ReactiveSet extends Set {
return false; return false;
} }
return get(source); return get(s);
} }
/** @param {T} value */ /** @param {T} value */
@ -110,12 +106,12 @@ export class ReactiveSet extends Set {
/** @param {T} value */ /** @param {T} value */
delete(value) { delete(value) {
var sources = this.#sources; var sources = this.#sources;
var source = sources.get(value); var s = sources.get(value);
if (source !== undefined) { if (s !== undefined) {
sources.delete(value); sources.delete(value);
set(this.#size, sources.size); set(this.#size, sources.size);
set(source, false); set(s, false);
this.#increment_version(); this.#increment_version();
} }
@ -127,8 +123,8 @@ export class ReactiveSet extends Set {
if (sources.size !== 0) { if (sources.size !== 0) {
set(this.#size, 0); set(this.#size, 0);
for (var source of sources.values()) { for (var s of sources.values()) {
set(source, false); set(s, false);
} }
this.#increment_version(); this.#increment_version();
} }
@ -138,45 +134,20 @@ export class ReactiveSet extends Set {
} }
keys() { keys() {
return this.values(); get(this.#version);
return this.#sources.keys();
} }
values() { values() {
get(this.#version); return this.keys();
var iterator = this.#sources.keys();
return make_iterable(
/** @type {IterableIterator<T>} */ ({
next() {
for (var value of iterator) {
return { value, done: false };
}
return { done: true };
}
})
);
} }
entries() { entries() {
var iterator = this.values(); return map(this.keys(), (key) => /** @type {[T, T]} */ ([key, key]));
return make_iterable(
/** @type {IterableIterator<[T, T]>} */ ({
next() {
for (var value of iterator) {
return { value: [value, value], done: false };
}
return { done: true };
}
})
);
} }
[Symbol.iterator]() { [Symbol.iterator]() {
return this.values(); return this.keys();
} }
get size() { get size() {

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

@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version * https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string} * @type {string}
*/ */
export const VERSION = '5.0.0-next.78'; export const VERSION = '5.0.0-next.80';
export const PUBLIC_VERSION = '5'; export const PUBLIC_VERSION = '5';

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

@ -1808,6 +1808,48 @@ declare module 'svelte/motion' {
export function tweened<T>(value?: T | undefined, defaults?: TweenedOptions<T> | undefined): Tweened<T>; export function tweened<T>(value?: T | undefined, defaults?: TweenedOptions<T> | undefined): Tweened<T>;
} }
declare module 'svelte/reactivity' {
export class Date extends Date {
constructor(...values: any[]);
#private;
}
export class Set<T> extends Set<any> {
constructor(value?: Iterable<T> | null | undefined);
has(value: T): boolean;
add(value: T): this;
delete(value: T): boolean;
keys(): IterableIterator<T>;
values(): IterableIterator<T>;
entries(): IterableIterator<[T, T]>;
[Symbol.iterator](): IterableIterator<T>;
#private;
}
export class Map<K, V> extends Map<any, any> {
constructor(value?: Iterable<readonly [K, V]> | null | undefined);
has(key: K): boolean;
forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, this_arg?: any): void;
get(key: K): V | undefined;
set(key: K, value: V): this;
delete(key: K): boolean;
keys(): IterableIterator<K>;
values(): IterableIterator<V>;
entries(): IterableIterator<[K, V]>;
[Symbol.iterator](): IterableIterator<[K, V]>;
#private;
}
}
declare module 'svelte/server' { declare module 'svelte/server' {
export function render(component: (...args: any[]) => void, options: { export function render(component: (...args: any[]) => void, options: {
props: Record<string, any>; props: Record<string, any>;

@ -93,6 +93,25 @@ This can improve performance with large arrays and objects that you weren't plan
> Objects and arrays passed to `$state.frozen` will be shallowly frozen using `Object.freeze()`. If you don't want this, pass in a clone of the object or array instead. > Objects and arrays passed to `$state.frozen` will be shallowly frozen using `Object.freeze()`. If you don't want this, pass in a clone of the object or array instead.
### Reactive Map, Set and Date
Svelte provides reactive `Map`, `Set` and `Date` classes. These can be imported from `svelte/reactivity` and used just like their native counterparts.
```svelte
<script>
import { Map } from 'svelte/reactivity';
const map = new Map();
map.set('message', 'hello');
function update_message() {
map.set('message', 'goodbye');
}
</script>
<p>{map.get('message')}</p>
```
## `$derived` ## `$derived`
Derived state is declared with the `$derived` rune: Derived state is declared with the `$derived` rune:

@ -103,7 +103,17 @@ No. You can do the migration towards runes incrementally when Svelte 5 comes out
### When can I `npm install` the Svelte 5 preview? ### When can I `npm install` the Svelte 5 preview?
We plan to publish a pre-release version with enough time for brave souls to try it out in their apps and give us feedback on what breaks. Watch this space. Right now!
```bash
npm install svelte@next
```
You can also opt into Svelte 5 when creating a new SvelteKit project:
```bash
npm create svelte@latest
```
### What's left to do? ### What's left to do?
@ -119,7 +129,7 @@ We know that some of you are very keen on certain feature ideas, and we are too.
### I want to help. How do I contribute? ### I want to help. How do I contribute?
We appreciate your enthusiasm! Right now it's not possible to accept contributions, but once we enter public beta, everything will be available on the Svelte GitHub repository. We appreciate your enthusiasm! We welcome issues on the [sveltejs/svelte](https://github.com/sveltejs/svelte) repo. Pull requests are a little dicier right now since many things are in flux, so we recommended starting with an issue.
### How can I share feedback or cool examples of what this enables? ### How can I share feedback or cool examples of what this enables?

Loading…
Cancel
Save