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-lies-camp",
"funny-wombats-argue",
"fuzzy-bags-camp",
"gentle-dolls-juggle",
"gentle-sheep-hug",
"gentle-spies-happen",
@ -234,6 +235,7 @@
"selfish-dragons-knock",
"selfish-tools-hide",
"serious-kids-deliver",
"serious-needles-joke",
"serious-socks-cover",
"serious-zebras-scream",
"seven-deers-jam",
@ -344,6 +346,7 @@
"wild-foxes-wonder",
"wise-apples-care",
"wise-dancers-hang",
"wise-dodos-tell",
"wise-donkeys-marry",
"wise-jobs-admire",
"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
## 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
### Patch Changes

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

@ -29,6 +29,7 @@ await createBundle({
[`${pkg.name}/easing`]: `${dir}/src/easing/index.js`,
[`${pkg.name}/legacy`]: `${dir}/src/legacy/legacy-client.js`,
[`${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}/store`]: `${dir}/src/store/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}
*/
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) {
return handler.call(this, event);
}

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

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

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

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

Loading…
Cancel
Save