diff --git a/.changeset/omit-bind-focused-ssr.md b/.changeset/omit-bind-focused-ssr.md new file mode 100644 index 0000000000..80200808aa --- /dev/null +++ b/.changeset/omit-bind-focused-ssr.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: omit `bind:focused` from SSR output (it has no HTML attribute) diff --git a/.changeset/polite-zoos-melt.md b/.changeset/polite-zoos-melt.md new file mode 100644 index 0000000000..cfff0e82e4 --- /dev/null +++ b/.changeset/polite-zoos-melt.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: properly apply static textarea value attribute during CSR diff --git a/.changeset/short-feet-admire.md b/.changeset/short-feet-admire.md new file mode 100644 index 0000000000..fdf56477c9 --- /dev/null +++ b/.changeset/short-feet-admire.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add `has` function to `createContext` diff --git a/.changeset/tasty-years-hammer.md b/.changeset/tasty-years-hammer.md new file mode 100644 index 0000000000..42b559ac9f --- /dev/null +++ b/.changeset/tasty-years-hammer.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add getOrInsert/getOrInsertComputed to SvelteMap diff --git a/documentation/docs/06-runtime/02-context.md b/documentation/docs/06-runtime/02-context.md index a1e0347b7b..f67339c6cf 100644 --- a/documentation/docs/06-runtime/02-context.md +++ b/documentation/docs/06-runtime/02-context.md @@ -4,7 +4,7 @@ title: Context Context allows components to access values owned by parent components without passing them down as props (potentially through many layers of intermediate components, known as 'prop-drilling'). -By creating a `[get, set]` pair of functions with `createContext`, you can set the context in a parent component and get it in a child component: +By creating a `[get, set, has]` triplet of functions with `createContext`, you can set the context in a parent component and get it in a child component: ```svelte diff --git a/documentation/docs/98-reference/.generated/shared-errors.md b/documentation/docs/98-reference/.generated/shared-errors.md index d7596fc068..44616d7c8d 100644 --- a/documentation/docs/98-reference/.generated/shared-errors.md +++ b/documentation/docs/98-reference/.generated/shared-errors.md @@ -78,7 +78,7 @@ Certain lifecycle methods can only be used during component initialisation. To f Context was not set in the current component or any of its ancestors ``` -The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in the current component or any of its ancestors. +The [`createContext()`](svelte#createContext) utility returns a `[get, set, has]` triplet of functions. `get` will throw an error if `set` was not used to set the context in the current component or any of its ancestors. ### snippet_without_render_tag diff --git a/package.json b/package.json index 16fc26bc32..8004698dd1 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "eslint-plugin-lube": "^0.5.1", "eslint-plugin-svelte": "^3.15.0", "jsdom": "25.0.1", - "playwright": "^1.60.0", + "playwright": "^1.62.0", "prettier": "^3.2.4", "prettier-plugin-svelte": "^3.4.0", "svelte": "workspace:^", diff --git a/packages/svelte/messages/shared-errors/errors.md b/packages/svelte/messages/shared-errors/errors.md index 43dca57ac8..e005e34fc8 100644 --- a/packages/svelte/messages/shared-errors/errors.md +++ b/packages/svelte/messages/shared-errors/errors.md @@ -64,7 +64,7 @@ Certain lifecycle methods can only be used during component initialisation. To f > Context was not set in the current component or any of its ancestors -The [`createContext()`](svelte#createContext) utility returns a `[get, set]` pair of functions. `get` will throw an error if `set` was not used to set the context in the current component or any of its ancestors. +The [`createContext()`](svelte#createContext) utility returns a `[get, set, has]` triplet of functions. `get` will throw an error if `set` was not used to set the context in the current component or any of its ancestors. ## snippet_without_render_tag diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 501440467e..17c97baf5b 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -159,7 +159,7 @@ }, "devDependencies": { "@jridgewell/trace-mapping": "^0.3.25", - "@playwright/test": "^1.60.0", + "@playwright/test": "^1.62.0", "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-node-resolve": "^15.3.0", "@rollup/plugin-terser": "^0.4.4", diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 9fd8c08881..b6cd67563d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -247,6 +247,7 @@ export function RegularElement(node, context) { if ( !is_custom_element && !cannot_be_set_statically(attribute.name) && + (name !== 'value' || node.name !== 'textarea') && (attribute.value === true || is_text_attribute(attribute)) && (name !== 'class' || class_directives.length === 0) && (name !== 'style' || style_directives.length === 0) diff --git a/packages/svelte/src/compiler/phases/bindings.js b/packages/svelte/src/compiler/phases/bindings.js index 6714a6e77d..dfe78da98c 100644 --- a/packages/svelte/src/compiler/phases/bindings.js +++ b/packages/svelte/src/compiler/phases/bindings.js @@ -23,7 +23,9 @@ export const binding_properties = { event: 'durationchange', omit_in_ssr: true }, - focused: {}, + focused: { + omit_in_ssr: true // no corresponding HTML attribute + }, paused: { valid_elements: ['audio', 'video'], omit_in_ssr: true, diff --git a/packages/svelte/src/internal/client/context.js b/packages/svelte/src/internal/client/context.js index e1571862ed..c3bb660e8c 100644 --- a/packages/svelte/src/internal/client/context.js +++ b/packages/svelte/src/internal/client/context.js @@ -72,17 +72,17 @@ export function set_dev_current_component_function(fn) { } /** - * Returns a `[get, set]` pair of functions for working with context in a type-safe way. + * Returns a `[get, set, has]` triplet of functions for working with context in a type-safe way. * * `get` will throw an error if `set` has not yet been called in the current component or any of * its ancestors. * * @template T - * @returns {[() => T, (context: T) => T]} + * @returns {[() => T, (context: T) => T, () => boolean]} * @since 5.40.0 */ export function createContext() { - return /** @type {[() => T, (context: T) => T]} */ ( + return /** @type {[() => T, (context: T) => T, () => boolean]} */ ( create_context(getContext, setContext, hasContext) ); } diff --git a/packages/svelte/src/internal/server/context.js b/packages/svelte/src/internal/server/context.js index de11c11282..b204c4c138 100644 --- a/packages/svelte/src/internal/server/context.js +++ b/packages/svelte/src/internal/server/context.js @@ -12,11 +12,11 @@ export function set_ssr_context(v) { /** * @template T - * @returns {[() => T, (context: T) => T]} + * @returns {[() => T, (context: T) => T, () => boolean]} * @since 5.40.0 */ export function createContext() { - return /** @type {[() => T, (context: T) => T]} */ ( + return /** @type {[() => T, (context: T) => T, () => boolean]} */ ( create_context(getContext, setContext, hasContext) ); } diff --git a/packages/svelte/src/internal/shared/context.js b/packages/svelte/src/internal/shared/context.js index 943e15e415..005c009101 100644 --- a/packages/svelte/src/internal/shared/context.js +++ b/packages/svelte/src/internal/shared/context.js @@ -5,7 +5,7 @@ import { lifecycle_outside_component, missing_context } from './errors.js'; * @param {(key: object) => T} get_context * @param {(key: object, context: T) => T} set_context * @param {(key: object) => boolean} has_context - * @returns {[() => T, (context: T) => T]} + * @returns {[() => T, (context: T) => T, () => boolean]} */ export function create_context(get_context, set_context, has_context) { const key = {}; @@ -18,7 +18,8 @@ export function create_context(get_context, set_context, has_context) { return get_context(key); }, - (context) => set_context(key, context) + (context) => set_context(key, context), + () => has_context(key) ]; } diff --git a/packages/svelte/src/reactivity/map.js b/packages/svelte/src/reactivity/map.js index 48d06a05a7..935db8fcb1 100644 --- a/packages/svelte/src/reactivity/map.js +++ b/packages/svelte/src/reactivity/map.js @@ -153,6 +153,28 @@ export class SvelteMap extends Map { return super.get(key); } + /** + * @param {K} key + * @param {V} value + * */ + getOrInsert(key, value) { + if (!super.has(key)) { + this.set(key, value); + } + return /** @type {V} */ (this.get(key)); + } + + /** + * @param {K} key + * @param {(key: K) => V} callbackFn + */ + getOrInsertComputed(key, callbackFn) { + if (!super.has(key)) { + this.set(key, callbackFn(key)); + } + return /** @type {V} */ (this.get(key)); + } + /** * @param {K} key * @param {V} value diff --git a/packages/svelte/src/reactivity/map.test.ts b/packages/svelte/src/reactivity/map.test.ts index 8bb6f72f7b..f5cbb21923 100644 --- a/packages/svelte/src/reactivity/map.test.ts +++ b/packages/svelte/src/reactivity/map.test.ts @@ -100,6 +100,85 @@ test('map.get(...)', () => { cleanup(); }); +test('map.getOrInsert(...)', () => { + const map = new SvelteMap([ + [2, 2], + [3, 3] + ]); + + const log: any = []; + + const cleanup = effect_root(() => { + render_effect(() => { + log.push('get 1', map.getOrInsert(1, 1)); + }); + + render_effect(() => { + log.push('get 2', map.getOrInsert(2, 2)); + }); + + render_effect(() => { + log.push('get 3', map.getOrInsert(3, 4)); + }); + }); + + flushSync(() => { + map.delete(2); + }); + + flushSync(() => { + map.set(2, 2); + }); + + assert.deepEqual(log, ['get 1', 1, 'get 2', 2, 'get 3', 3, 'get 2', 2]); + + cleanup(); +}); + +test('map.getOrInsertComputed(...)', () => { + const map = new SvelteMap([ + [2, 2], + [3, 3] + ]); + + const log: any = []; + + const cleanup = effect_root(() => { + render_effect(() => { + log.push( + 'get 1', + map.getOrInsertComputed(1, (k) => k) + ); + }); + + render_effect(() => { + log.push( + 'get 2', + map.getOrInsertComputed(2, (k) => k) + ); + }); + + render_effect(() => { + log.push( + 'get 3', + map.getOrInsertComputed(3, () => 4) + ); + }); + }); + + flushSync(() => { + map.delete(2); + }); + + flushSync(() => { + map.set(2, 2); + }); + + assert.deepEqual(log, ['get 1', 1, 'get 2', 2, 'get 3', 3, 'get 2', 2]); + + cleanup(); +}); + test('map.has(...)', () => { const map = new SvelteMap([ [1, 1], diff --git a/packages/svelte/tests/runtime-legacy/samples/textarea-value/_config.js b/packages/svelte/tests/runtime-legacy/samples/textarea-value/_config.js index 06264b653b..30e2633154 100644 --- a/packages/svelte/tests/runtime-legacy/samples/textarea-value/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/textarea-value/_config.js @@ -5,14 +5,19 @@ export default test({ return { foo: 42 }; }, - html: '', - ssrHtml: '', + ssrHtml: ' ', - test({ assert, component, target }) { - const textarea = /** @type {HTMLTextAreaElement} */ (target.querySelector('textarea')); - assert.strictEqual(textarea.value, '42'); + test({ assert, component, target, variant }) { + assert.htmlEqual( + target.innerHTML, + ` ` + ); + + const [textarea1, textarea2] = target.querySelectorAll('textarea'); + assert.strictEqual(textarea1.value, '42'); + assert.strictEqual(textarea2.value, 'static'); component.foo = 43; - assert.strictEqual(textarea.value, '43'); + assert.strictEqual(textarea1.value, '43'); } }); diff --git a/packages/svelte/tests/runtime-legacy/samples/textarea-value/main.svelte b/packages/svelte/tests/runtime-legacy/samples/textarea-value/main.svelte index 7eaf783f7f..203a3ed039 100644 --- a/packages/svelte/tests/runtime-legacy/samples/textarea-value/main.svelte +++ b/packages/svelte/tests/runtime-legacy/samples/textarea-value/main.svelte @@ -2,4 +2,5 @@ export let foo; - diff --git a/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/Child.svelte b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/Child.svelte index 3e39d5043e..0255132ad6 100644 --- a/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/Child.svelte +++ b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/Child.svelte @@ -1,7 +1,13 @@

{message}

+{#if has()} +

it's me

+{/if} +{#if !has_unset()} +

or not

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/_config.js b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/_config.js index f4374c8759..c2eeca0de8 100644 --- a/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/_config.js @@ -2,7 +2,7 @@ import { test } from '../../test'; export default test({ ssrHtml: `
`, - html: `

hello

`, + html: `

hello

it's me

or not

`, test() {} }); diff --git a/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/main.svelte b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/main.svelte index 3c42dbf180..8f4fb67d23 100644 --- a/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/create-context-programmatic/main.svelte @@ -3,9 +3,11 @@ import Child from './Child.svelte'; /** @type {ReturnType>} */ - const [get, set] = createContext(); + const [get, set, has] = createContext(); + /** @type {ReturnType>} */ + const [, , has_unset] = createContext(); - export { get }; + export { get, has, has_unset }; function Wrapper(Component) { return (...args) => { @@ -15,6 +17,8 @@ } -
{ - mount(Wrapper(Child), { target }); -}}>
+
{ + mount(Wrapper(Child), { target }); + }} +>
diff --git a/packages/svelte/tests/runtime-runes/samples/create-context/Child.svelte b/packages/svelte/tests/runtime-runes/samples/create-context/Child.svelte index 3e39d5043e..0255132ad6 100644 --- a/packages/svelte/tests/runtime-runes/samples/create-context/Child.svelte +++ b/packages/svelte/tests/runtime-runes/samples/create-context/Child.svelte @@ -1,7 +1,13 @@

{message}

+{#if has()} +

it's me

+{/if} +{#if !has_unset()} +

or not

+{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/create-context/_config.js b/packages/svelte/tests/runtime-runes/samples/create-context/_config.js index 4ae28e68bd..fa782095de 100644 --- a/packages/svelte/tests/runtime-runes/samples/create-context/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/create-context/_config.js @@ -1,5 +1,5 @@ import { test } from '../../test'; export default test({ - html: `

hello

` + html: `

hello

it's me

or not

` }); diff --git a/packages/svelte/tests/runtime-runes/samples/create-context/main.svelte b/packages/svelte/tests/runtime-runes/samples/create-context/main.svelte index 8d3c50ba55..35093f4c4e 100644 --- a/packages/svelte/tests/runtime-runes/samples/create-context/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/create-context/main.svelte @@ -2,9 +2,11 @@ import { createContext } from 'svelte'; /** @type {ReturnType>} */ - const [get, set] = createContext(); + const [get, set, has] = createContext(); + /** @type {ReturnType>} */ + const [, , has_unset] = createContext(); - export { get }; + export { get, has, has_unset };