diff --git a/.changeset/clever-ghosts-laugh.md b/.changeset/clever-ghosts-laugh.md deleted file mode 100644 index 3de871ba79..0000000000 --- a/.changeset/clever-ghosts-laugh.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: ensure `svelte:component` evaluates props once diff --git a/.changeset/lucky-knives-crash.md b/.changeset/lucky-knives-crash.md deleted file mode 100644 index b3506ecc4f..0000000000 --- a/.changeset/lucky-knives-crash.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: handle destructured primitive literals diff --git a/README.md b/README.md index a5e17f71e4..4dbfee3823 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ pnpm test -- -g transition ### svelte.dev -The source code for https://svelte.dev lives in the [sites](https://github.com/sveltejs/sites) repository, with all the documentation in the [site/content](site/content) directory. The site is built with [SvelteKit](https://kit.svelte.dev). +The source code for https://svelte.dev lives in the [sites](https://github.com/sveltejs/svelte/tree/master/sites/svelte.dev) folder, with all the documentation right [here](https://github.com/sveltejs/svelte/tree/master/documentation). The site is built with [SvelteKit](https://kit.svelte.dev). ## Is svelte.dev down? diff --git a/documentation/docs/04-compiler-and-api/04-custom-elements-api.md b/documentation/docs/04-compiler-and-api/04-custom-elements-api.md index 12f51b6440..2227ae83c2 100644 --- a/documentation/docs/04-compiler-and-api/04-custom-elements-api.md +++ b/documentation/docs/04-compiler-and-api/04-custom-elements-api.md @@ -55,13 +55,28 @@ console.log(el.name); el.name = 'everybody'; ``` +## Component lifecycle + +Custom elements are created from Svelte components using a wrapper approach. This means the inner Svelte component has no knowledge that it is a custom element. The custom element wrapper takes care of handling its lifecycle appropriately. + +When a custom element is created, the Svelte component it wraps is _not_ created right away. It is only created in the next tick after the `connectedCallback` is invoked. Properties assigned to the custom element before it is inserted into the DOM are temporarily saved and then set on component creation, so their values are not lost. The same does not work for invoking exported functions on the custom element though, they are only available after the element has mounted. If you need to invoke functions before component creation, you can work around it by using the [`extend` option](#component-options). + +When a custom element written with Svelte is created or updated, the shadow DOM will reflect the value in the next tick, not immediately. This way updates can be batched, and DOM moves which temporarily (but synchronously) detach the element from the DOM don't lead to unmounting the inner component. + +The inner Svelte component is destroyed in the next tick after the `disconnectedCallback` is invoked. + ## Component options -When constructing a custom element, you can tailor several aspects by defining `customElement` as an object within `` since Svelte 4. This object comprises a mandatory `tag` property for the custom element's name, an optional `shadow` property that can be set to `"none"` to forgo shadow root creation (note that styles are then no longer encapsulated, and you can't use slots), and a `props` option, which offers the following settings: +When constructing a custom element, you can tailor several aspects by defining `customElement` as an object within `` since Svelte 4. This object may contain the following properties: -- `attribute: string`: To update a custom element's prop, you have two alternatives: either set the property on the custom element's reference as illustrated above or use an HTML attribute. For the latter, the default attribute name is the lowercase property name. Modify this by assigning `attribute: ""`. -- `reflect: boolean`: By default, updated prop values do not reflect back to the DOM. To enable this behavior, set `reflect: true`. -- `type: 'String' | 'Boolean' | 'Number' | 'Array' | 'Object'`: While converting an attribute value to a prop value and reflecting it back, the prop value is assumed to be a `String` by default. This may not always be accurate. For instance, for a number type, define it using `type: "Number"` +- `tag`: the mandatory `tag` property for the custom element's name +- `shadow`: an optional property that can be set to `"none"` to forgo shadow root creation. Note that styles are then no longer encapsulated, and you can't use slots +- `props`: an optional property to modify certain details and behaviors of your component's properties. It offers the following settings: + - `attribute: string`: To update a custom element's prop, you have two alternatives: either set the property on the custom element's reference as illustrated above or use an HTML attribute. For the latter, the default attribute name is the lowercase property name. Modify this by assigning `attribute: ""`. + - `reflect: boolean`: By default, updated prop values do not reflect back to the DOM. To enable this behavior, set `reflect: true`. + - `type: 'String' | 'Boolean' | 'Number' | 'Array' | 'Object'`: While converting an attribute value to a prop value and reflecting it back, the prop value is assumed to be a `String` by default. This may not always be accurate. For instance, for a number type, define it using `type: "Number"` + You don't need to list all properties, those not listed will use the default settings. +- `extend`: an optional property which expects a function as its argument. It is passed the custom element class generated by Svelte and expects you to return a custom element class. This comes in handy if you have very specific requirements to the life cycle of the custom element or want to enhance the class to for example use [ElementInternals](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals#examples) for better HTML form integration. ```svelte { + // Extend the class so we can let it participate in HTML forms + return class extends customElementConstructor { + static formAssociated = true; + + constructor() { + super(); + this.attachedInternals = this.attachInternals(); + } + + // Add the function here, not below in the component so that + // it's always available, not just when the inner Svelte component + // is mounted + randomIndex() { + this.elementIndex = Math.random(); + } + }; } }} /> ... @@ -91,5 +129,4 @@ Custom elements can be a useful way to package components for consumption in a n - In Svelte, slotted content renders _lazily_. In the DOM, it renders _eagerly_. In other words, it will always be created even if the component's `` element is inside an `{#if ...}` block. Similarly, including a `` in an `{#each ...}` block will not cause the slotted content to be rendered multiple times - The `let:` directive has no effect, because custom elements do not have a way to pass data to the parent component that fills the slot - Polyfills are required to support older browsers - -When a custom element written with Svelte is created or updated, the shadow dom will reflect the value in the next tick, not immediately. This way updates can be batched, and DOM moves which temporarily (but synchronously) detach the element from the DOM don't lead to unmounting the inner component. +- You can use Svelte's context feature between regular Svelte components within a custom element, but you can't use them across custom elements. In other words, you can't use `setContext` on a parent custom element and read that with `getContext` in a child custom element. diff --git a/documentation/docs/05-misc/03-typescript.md b/documentation/docs/05-misc/03-typescript.md index 5d164324a3..d5772fa157 100644 --- a/documentation/docs/05-misc/03-typescript.md +++ b/documentation/docs/05-misc/03-typescript.md @@ -97,8 +97,8 @@ Events can be typed with `createEventDispatcher`: const dispatch = createEventDispatcher<{ event: null; // does not accept a payload - type: string; // has a required string payload - click: string | null; // has an optional string payload + click: string; // has a required string payload + type: string | null; // has an optional string payload }>(); function handleClick() { diff --git a/documentation/examples/09-transitions/06-deferred-transitions/App.svelte b/documentation/examples/09-transitions/06-deferred-transitions/App.svelte index 74db062fc8..8f603165c1 100644 --- a/documentation/examples/09-transitions/06-deferred-transitions/App.svelte +++ b/documentation/examples/09-transitions/06-deferred-transitions/App.svelte @@ -48,7 +48,7 @@ {#if selected} {#await selected then d} -
+
{d.alt} (selected = null)} /> diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 2b76f02650..4c5b4382f7 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,29 @@ # svelte +## 4.1.1 + +### Patch Changes + +- fix: `svelte:component` spread props change not picked up ([#9006](https://github.com/sveltejs/svelte/pull/9006)) + +## 4.1.0 + +### Minor Changes + +- feat: add ability to extend custom element class ([#8991](https://github.com/sveltejs/svelte/pull/8991)) + +### Patch Changes + +- fix: ensure `svelte:component` evaluates props once ([#8946](https://github.com/sveltejs/svelte/pull/8946)) + +- fix: remove `let:variable` slot bindings from select binding dependencies ([#8969](https://github.com/sveltejs/svelte/pull/8969)) + +- fix: handle destructured primitive literals ([#8871](https://github.com/sveltejs/svelte/pull/8871)) + +- perf: optimize imports that are not mutated or reassigned ([#8948](https://github.com/sveltejs/svelte/pull/8948)) + +- fix: don't add accessor twice ([#8996](https://github.com/sveltejs/svelte/pull/8996)) + ## 4.0.5 ### Patch Changes diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 046ba94786..ec450284c5 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -1676,6 +1676,9 @@ export interface SvelteHTMLElements { } > | undefined; + extend?: ( + svelteCustomElementClass: new () => HTMLElement + ) => new () => HTMLElement | undefined; }; immutable?: boolean | undefined; accessors?: boolean | undefined; diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c869a3caa8..c3621ab6dc 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "svelte", - "version": "4.0.5", + "version": "4.1.1", "description": "Cybernetically enhanced web apps", "type": "module", "module": "src/runtime/index.js", diff --git a/packages/svelte/src/compiler/compile/Component.js b/packages/svelte/src/compiler/compile/Component.js index 06e849fe13..7598e60967 100644 --- a/packages/svelte/src/compiler/compile/Component.js +++ b/packages/svelte/src/compiler/compile/Component.js @@ -770,14 +770,12 @@ export default class Component { if (name[0] === '$') { return this.error(/** @type {any} */ (node), compiler_errors.illegal_declaration); } - const writable = - node.type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let'); - const imported = node.type.startsWith('Import'); + const { type } = node; this.add_var(node, { name, initialised: instance_scope.initialised_declarations.has(name), - writable, - imported + imported: type.startsWith('Import'), + writable: type === 'VariableDeclaration' && (node.kind === 'var' || node.kind === 'let') }); this.node_for_declaration.set(name, node); }); @@ -1710,6 +1708,7 @@ function process_component_options(component, nodes) { case 'customElement': { component_options.customElement = component_options.customElement || /** @type {any} */ ({}); + const { value } = attribute; if (value[0].type === 'MustacheTag' && value[0].expression?.value === null) { component_options.customElement.tag = null; @@ -1720,12 +1719,14 @@ function process_component_options(component, nodes) { } else if (value[0].expression.type !== 'ObjectExpression') { return component.error(attribute, compiler_errors.invalid_customElement_attribute); } + const tag = value[0].expression.properties.find((prop) => prop.key.name === 'tag'); if (tag) { parse_tag(tag, tag.value?.value); } else { return component.error(attribute, compiler_errors.invalid_customElement_attribute); } + const props = value[0].expression.properties.find((prop) => prop.key.name === 'props'); if (props) { const error = () => @@ -1770,6 +1771,7 @@ function process_component_options(component, nodes) { } } } + const shadow = value[0].expression.properties.find( (prop) => prop.key.name === 'shadow' ); @@ -1780,6 +1782,14 @@ function process_component_options(component, nodes) { } component_options.customElement.shadow = shadowdom; } + + const extend = value[0].expression.properties.find( + (prop) => prop.key.name === 'extend' + ); + if (extend?.value) { + component_options.customElement.extend = extend.value; + } + break; } case 'namespace': { @@ -1853,7 +1863,8 @@ function get_sourcemap_source_filename(compile_options) { : get_basename(compile_options.filename); } -/** @typedef {Object} ComponentOptions +/** + * @typedef {Object} ComponentOptions * @property {string} [namespace] * @property {boolean} [immutable] * @property {boolean} [accessors] @@ -1862,4 +1873,5 @@ function get_sourcemap_source_filename(compile_options) { * @property {string|null} customElement.tag * @property {'open'|'none'} [customElement.shadow] * @property {Record} [customElement.props] + * @property {(ceClass: new () => HTMLElement) => new () => HTMLElement} [customElement.extend] */ diff --git a/packages/svelte/src/compiler/compile/internal_exports.js b/packages/svelte/src/compiler/compile/internal_exports.js index d2b7356214..7656f52f3a 100644 --- a/packages/svelte/src/compiler/compile/internal_exports.js +++ b/packages/svelte/src/compiler/compile/internal_exports.js @@ -1,2 +1,2 @@ // This file is automatically generated -export default new Set(["HtmlTag","HtmlTagHydration","ResizeObserverSingleton","SvelteComponent","SvelteComponentDev","SvelteComponentTyped","SvelteElement","action_destroyer","add_attribute","add_classes","add_flush_callback","add_iframe_resize_listener","add_location","add_render_callback","add_styles","add_transform","afterUpdate","append","append_dev","append_empty_stylesheet","append_hydration","append_hydration_dev","append_styles","assign","attr","attr_dev","attribute_to_object","beforeUpdate","bind","binding_callbacks","blank_object","bubble","check_outros","children","claim_comment","claim_component","claim_element","claim_html_tag","claim_space","claim_svg_element","claim_text","clear_loops","comment","component_subscribe","compute_rest_props","compute_slots","construct_svelte_component","construct_svelte_component_dev","contenteditable_truthy_values","createEventDispatcher","create_animation","create_bidirectional_transition","create_component","create_custom_element","create_in_transition","create_out_transition","create_slot","create_ssr_component","current_component","custom_event","dataset_dev","debug","destroy_block","destroy_component","destroy_each","detach","detach_after_dev","detach_before_dev","detach_between_dev","detach_dev","dirty_components","dispatch_dev","each","element","element_is","empty","end_hydrating","ensure_array_like","ensure_array_like_dev","escape","escape_attribute_value","escape_object","exclude_internal_props","fix_and_destroy_block","fix_and_outro_and_destroy_block","fix_position","flush","flush_render_callbacks","getAllContexts","getContext","get_all_dirty_from_scope","get_binding_group_value","get_current_component","get_custom_elements_slots","get_root_for_style","get_slot_changes","get_spread_object","get_spread_update","get_store_value","get_svelte_dataset","globals","group_outros","handle_promise","hasContext","has_prop","head_selector","identity","init","init_binding_group","init_binding_group_dynamic","insert","insert_dev","insert_hydration","insert_hydration_dev","intros","invalid_attribute_name_character","is_client","is_crossorigin","is_empty","is_function","is_promise","is_void","listen","listen_dev","loop","loop_guard","merge_ssr_styles","missing_component","mount_component","noop","not_equal","now","null_to_empty","object_without_properties","onDestroy","onMount","once","outro_and_destroy_block","prevent_default","prop_dev","query_selector_all","raf","resize_observer_border_box","resize_observer_content_box","resize_observer_device_pixel_content_box","run","run_all","safe_not_equal","schedule_update","select_multiple_value","select_option","select_options","select_value","self","setContext","set_attributes","set_current_component","set_custom_element_data","set_custom_element_data_map","set_data","set_data_contenteditable","set_data_contenteditable_dev","set_data_dev","set_data_maybe_contenteditable","set_data_maybe_contenteditable_dev","set_dynamic_element_data","set_input_type","set_input_value","set_now","set_raf","set_store_value","set_style","set_svg_attributes","space","split_css_unit","spread","src_url_equal","start_hydrating","stop_immediate_propagation","stop_propagation","subscribe","svg_element","text","tick","time_ranges_to_array","to_number","toggle_class","transition_in","transition_out","trusted","update_await_block_branch","update_keyed_each","update_slot","update_slot_base","validate_component","validate_dynamic_element","validate_each_keys","validate_slots","validate_store","validate_void_dynamic_element","xlink_attr"]); \ No newline at end of file +export default new Set(["HtmlTag","HtmlTagHydration","ResizeObserverSingleton","SvelteComponent","SvelteComponentDev","SvelteComponentTyped","SvelteElement","action_destroyer","add_attribute","add_classes","add_flush_callback","add_iframe_resize_listener","add_location","add_render_callback","add_styles","add_transform","afterUpdate","append","append_dev","append_empty_stylesheet","append_hydration","append_hydration_dev","append_styles","assign","attr","attr_dev","attribute_to_object","beforeUpdate","bind","binding_callbacks","blank_object","bubble","check_outros","children","claim_comment","claim_component","claim_element","claim_html_tag","claim_space","claim_svg_element","claim_text","clear_loops","comment","component_subscribe","compute_rest_props","compute_slots","construct_svelte_component","construct_svelte_component_dev","contenteditable_truthy_values","createEventDispatcher","create_animation","create_bidirectional_transition","create_component","create_custom_element","create_in_transition","create_out_transition","create_slot","create_ssr_component","current_component","custom_event","dataset_dev","debug","destroy_block","destroy_component","destroy_each","detach","detach_after_dev","detach_before_dev","detach_between_dev","detach_dev","dirty_components","dispatch_dev","each","element","element_is","empty","end_hydrating","ensure_array_like","ensure_array_like_dev","escape","escape_attribute_value","escape_object","exclude_internal_props","fix_and_destroy_block","fix_and_outro_and_destroy_block","fix_position","flush","flush_render_callbacks","getAllContexts","getContext","get_all_dirty_from_scope","get_binding_group_value","get_current_component","get_custom_elements_slots","get_root_for_style","get_slot_changes","get_spread_object","get_spread_update","get_store_value","get_svelte_dataset","globals","group_outros","handle_promise","hasContext","has_prop","head_selector","identity","init","init_binding_group","init_binding_group_dynamic","insert","insert_dev","insert_hydration","insert_hydration_dev","intros","invalid_attribute_name_character","is_client","is_crossorigin","is_empty","is_function","is_promise","is_void","listen","listen_dev","loop","loop_guard","merge_ssr_styles","missing_component","mount_component","noop","not_equal","now","null_to_empty","object_without_properties","onDestroy","onMount","once","outro_and_destroy_block","prevent_default","prop_dev","query_selector_all","raf","resize_observer_border_box","resize_observer_content_box","resize_observer_device_pixel_content_box","run","run_all","safe_not_equal","schedule_update","select_multiple_value","select_option","select_options","select_value","self","setContext","set_attributes","set_current_component","set_custom_element_data","set_custom_element_data_map","set_data","set_data_contenteditable","set_data_contenteditable_dev","set_data_dev","set_data_maybe_contenteditable","set_data_maybe_contenteditable_dev","set_dynamic_element_data","set_input_type","set_input_value","set_now","set_raf","set_store_value","set_style","set_svg_attributes","space","split_css_unit","spread","src_url_equal","srcset_url_equal","start_hydrating","stop_immediate_propagation","stop_propagation","subscribe","svg_element","text","tick","time_ranges_to_array","to_number","toggle_class","transition_in","transition_out","trusted","update_await_block_branch","update_keyed_each","update_slot","update_slot_base","validate_component","validate_dynamic_element","validate_each_keys","validate_slots","validate_store","validate_void_dynamic_element","xlink_attr"]); \ No newline at end of file diff --git a/packages/svelte/src/compiler/compile/nodes/shared/Expression.js b/packages/svelte/src/compiler/compile/nodes/shared/Expression.js index fe1feac534..69310f77b4 100644 --- a/packages/svelte/src/compiler/compile/nodes/shared/Expression.js +++ b/packages/svelte/src/compiler/compile/nodes/shared/Expression.js @@ -54,7 +54,8 @@ export default class Expression { /** @type {Array} */ declarations = []; - /** */ + + /** @type {boolean} */ uses_context = false; /** @type {import('estree').Node} */ @@ -129,7 +130,10 @@ export default class Expression { } } else { if (!lazy) { - dependencies.add(name); + const variable = component.var_lookup.get(name); + if (!variable || !variable.imported || variable.mutated || variable.reassigned) { + dependencies.add(name); + } } component.add_reference(node, name); component.warn_if_undefined(name, nodes[0], template_scope, owner); @@ -231,6 +235,8 @@ export default class Expression { if (this.manipulated) return this.manipulated; const { component, declarations, scope_map: map, template_scope, owner } = this; let scope = this.scope; + + /** @type {import('estree').FunctionExpression | import('estree').ArrowFunctionExpression | null} */ let function_expression; /** @type {Set} */ diff --git a/packages/svelte/src/compiler/compile/render_dom/index.js b/packages/svelte/src/compiler/compile/render_dom/index.js index 9a11013fc9..0627e7e0cb 100644 --- a/packages/svelte/src/compiler/compile/render_dom/index.js +++ b/packages/svelte/src/compiler/compile/render_dom/index.js @@ -583,25 +583,28 @@ export default function dom(component, options) { }, {}); const slots_str = [...component.slots.keys()].map((key) => `"${key}"`).join(','); const accessors_str = accessors - .filter((accessor) => !writable_props.some((prop) => prop.export_name === accessor.key.name)) + .filter( + (accessor) => + accessor.kind === 'get' && + !writable_props.some((prop) => prop.export_name === accessor.key.name) + ) .map((accessor) => `"${accessor.key.name}"`) .join(','); const use_shadow_dom = component.component_options.customElement?.shadow !== 'none' ? 'true' : 'false'; + + const create_ce = x`@create_custom_element(${name}, ${JSON.stringify( + props_str + )}, [${slots_str}], [${accessors_str}], ${use_shadow_dom}, ${ + component.component_options.customElement?.extend + })`; + if (component.component_options.customElement?.tag) { body.push( - b`@_customElements.define("${ - component.component_options.customElement.tag - }", @create_custom_element(${name}, ${JSON.stringify( - props_str - )}, [${slots_str}], [${accessors_str}], ${use_shadow_dom}));` + b`@_customElements.define("${component.component_options.customElement.tag}", ${create_ce});` ); } else { - body.push( - b`@create_custom_element(${name}, ${JSON.stringify( - props_str - )}, [${slots_str}], [${accessors_str}], ${use_shadow_dom});` - ); + body.push(b`${create_ce}`); } } diff --git a/packages/svelte/src/compiler/compile/render_dom/wrappers/Element/Attribute.js b/packages/svelte/src/compiler/compile/render_dom/wrappers/Element/Attribute.js index 0459b55b03..8d0199f0cf 100644 --- a/packages/svelte/src/compiler/compile/render_dom/wrappers/Element/Attribute.js +++ b/packages/svelte/src/compiler/compile/render_dom/wrappers/Element/Attribute.js @@ -91,7 +91,9 @@ export default class AttributeWrapper extends BaseAttributeWrapper { if (select && select.select_binding_dependencies) { select.select_binding_dependencies.forEach((prop) => { this.node.dependencies.forEach((dependency) => { - this.parent.renderer.component.indirect_dependencies.get(prop).add(dependency); + if (this.node.scope.is_top_level(dependency)) { + this.parent.renderer.component.indirect_dependencies.get(prop).add(dependency); + } }); }); } diff --git a/packages/svelte/src/compiler/compile/render_dom/wrappers/InlineComponent/index.js b/packages/svelte/src/compiler/compile/render_dom/wrappers/InlineComponent/index.js index ef7e5432d4..77fb1ac96b 100644 --- a/packages/svelte/src/compiler/compile/render_dom/wrappers/InlineComponent/index.js +++ b/packages/svelte/src/compiler/compile/render_dom/wrappers/InlineComponent/index.js @@ -268,6 +268,21 @@ export default class InlineComponentWrapper extends Wrapper { `); if (all_dependencies.size) { const condition = renderer.dirty(Array.from(all_dependencies)); + if (this.node.name === 'svelte:component') { + // statements will become switch_props function body + // rewrite last statement, add props update logic + statements[statements.length - 1] = b` + if (#dirty !== undefined && ${condition}) { + ${props} = @get_spread_update(${levels}, [ + ${changes} + ]); + } else { + for (let #i = 0; #i < ${levels}.length; #i += 1) { + ${props} = @assign(${props}, ${levels}[#i]); + } + } + `; + } updates.push(b` const ${name_changes} = ${condition} ? @get_spread_update(${levels}, [ ${changes} @@ -396,7 +411,7 @@ export default class InlineComponentWrapper extends Wrapper { block.chunks.init.push(b` var ${switch_value} = ${snippet}; - function ${switch_props}(#ctx) { + function ${switch_props}(#ctx, #dirty) { ${ (this.node.attributes.length > 0 || this.node.bindings.length > 0) && b` @@ -464,7 +479,7 @@ export default class InlineComponentWrapper extends Wrapper { if (${switch_value}) { ${update_insert} - ${name} = @construct_svelte_component(${switch_value}, ${switch_props}(#ctx)); + ${name} = @construct_svelte_component(${switch_value}, ${switch_props}(#ctx, #dirty)); ${munged_bindings} ${munged_handlers} diff --git a/packages/svelte/src/runtime/internal/Component.js b/packages/svelte/src/runtime/internal/Component.js index 4733b1d745..bc5b117c2f 100644 --- a/packages/svelte/src/runtime/internal/Component.js +++ b/packages/svelte/src/runtime/internal/Component.js @@ -383,15 +383,17 @@ function get_custom_element_value(prop, value, props_definition, transform) { * @param {string[]} slots The slots to create * @param {string[]} accessors Other accessors besides the ones for props the component has * @param {boolean} use_shadow_dom Whether to use shadow DOM + * @param {(ce: new () => HTMLElement) => new () => HTMLElement} [extend] */ export function create_custom_element( Component, props_definition, slots, accessors, - use_shadow_dom + use_shadow_dom, + extend ) { - const Class = class extends SvelteElement { + let Class = class extends SvelteElement { constructor() { super(Component, slots, use_shadow_dom); this.$$p_d = props_definition; @@ -421,6 +423,10 @@ export function create_custom_element( } }); }); + if (extend) { + // @ts-expect-error - assigning here is fine + Class = extend(Class); + } Component.element = /** @type {any} */ (Class); return Class; } diff --git a/packages/svelte/src/shared/version.js b/packages/svelte/src/shared/version.js index a9675ed742..e4e0dea0aa 100644 --- a/packages/svelte/src/shared/version.js +++ b/packages/svelte/src/shared/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '4.0.5'; +export const VERSION = '4.1.1'; export const PUBLIC_VERSION = '4'; diff --git a/packages/svelte/test/runtime-browser/custom-elements-samples/custom-class/main.svelte b/packages/svelte/test/runtime-browser/custom-elements-samples/custom-class/main.svelte new file mode 100644 index 0000000000..f110b118f4 --- /dev/null +++ b/packages/svelte/test/runtime-browser/custom-elements-samples/custom-class/main.svelte @@ -0,0 +1,22 @@ + { + return class extends CeClass { + updateFoo(value) { + this.foo = value; + } + }; + } + }} +/> + + + +

{foo}

diff --git a/packages/svelte/test/runtime-browser/custom-elements-samples/custom-class/test.js b/packages/svelte/test/runtime-browser/custom-elements-samples/custom-class/test.js new file mode 100644 index 0000000000..b794f02e25 --- /dev/null +++ b/packages/svelte/test/runtime-browser/custom-elements-samples/custom-class/test.js @@ -0,0 +1,14 @@ +import * as assert from 'assert.js'; +import { tick } from 'svelte'; +import './main.svelte'; + +export default async function (target) { + const element = document.createElement('custom-element'); + element.updateFoo('42'); + target.appendChild(element); + await tick(); + + const el = target.querySelector('custom-element'); + const p = el.shadowRoot.querySelector('p'); + assert.equal(p.textContent, '42'); +} diff --git a/packages/svelte/test/runtime/samples/binding-select-from-let-2/Parent.svelte b/packages/svelte/test/runtime/samples/binding-select-from-let-2/Parent.svelte new file mode 100644 index 0000000000..565c3bc6b7 --- /dev/null +++ b/packages/svelte/test/runtime/samples/binding-select-from-let-2/Parent.svelte @@ -0,0 +1,6 @@ + + + + diff --git a/packages/svelte/test/runtime/samples/binding-select-from-let-2/_config.js b/packages/svelte/test/runtime/samples/binding-select-from-let-2/_config.js new file mode 100644 index 0000000000..125455819a --- /dev/null +++ b/packages/svelte/test/runtime/samples/binding-select-from-let-2/_config.js @@ -0,0 +1,25 @@ +export default { + html: ` + +

1

+ `, + + async test({ assert, component, target, window }) { + const select = target.querySelector('select'); + const options = target.querySelectorAll('option'); + + assert.equal(component.tasks_touched, 1); + + const change = new window.Event('change'); + options[1].selected = true; + await select.dispatchEvent(change); + + assert.equal(component.selected, options[1].value); + assert.equal(component.tasks_touched, 1); + } +}; diff --git a/packages/svelte/test/runtime/samples/binding-select-from-let-2/main.svelte b/packages/svelte/test/runtime/samples/binding-select-from-let-2/main.svelte new file mode 100644 index 0000000000..3c21eba829 --- /dev/null +++ b/packages/svelte/test/runtime/samples/binding-select-from-let-2/main.svelte @@ -0,0 +1,19 @@ + + + + + +

{tasks_touched}

diff --git a/packages/svelte/test/runtime/samples/binding-select-from-let/Parent.svelte b/packages/svelte/test/runtime/samples/binding-select-from-let/Parent.svelte new file mode 100644 index 0000000000..565c3bc6b7 --- /dev/null +++ b/packages/svelte/test/runtime/samples/binding-select-from-let/Parent.svelte @@ -0,0 +1,6 @@ + + + + diff --git a/packages/svelte/test/runtime/samples/binding-select-from-let/_config.js b/packages/svelte/test/runtime/samples/binding-select-from-let/_config.js new file mode 100644 index 0000000000..30f42a2251 --- /dev/null +++ b/packages/svelte/test/runtime/samples/binding-select-from-let/_config.js @@ -0,0 +1,21 @@ +export default { + html: ` + + `, + + async test({ assert, component, target, window }) { + const select = target.querySelector('select'); + const options = target.querySelectorAll('option'); + + const change = new window.Event('change'); + options[1].selected = true; + await select.dispatchEvent(change); + + assert.equal(component.selected, options[1].value); + } +}; diff --git a/packages/svelte/test/runtime/samples/binding-select-from-let/main.svelte b/packages/svelte/test/runtime/samples/binding-select-from-let/main.svelte new file mode 100644 index 0000000000..494653bb4c --- /dev/null +++ b/packages/svelte/test/runtime/samples/binding-select-from-let/main.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/packages/svelte/test/runtime/samples/dynamic-component-spread-props/Comp1.svelte b/packages/svelte/test/runtime/samples/dynamic-component-spread-props/Comp1.svelte new file mode 100644 index 0000000000..d4fe28a8a3 --- /dev/null +++ b/packages/svelte/test/runtime/samples/dynamic-component-spread-props/Comp1.svelte @@ -0,0 +1,5 @@ + + +

value(1) = {value}

diff --git a/packages/svelte/test/runtime/samples/dynamic-component-spread-props/Comp2.svelte b/packages/svelte/test/runtime/samples/dynamic-component-spread-props/Comp2.svelte new file mode 100644 index 0000000000..07d41f3d84 --- /dev/null +++ b/packages/svelte/test/runtime/samples/dynamic-component-spread-props/Comp2.svelte @@ -0,0 +1,5 @@ + + +

value(2) = {value}

diff --git a/packages/svelte/test/runtime/samples/dynamic-component-spread-props/_config.js b/packages/svelte/test/runtime/samples/dynamic-component-spread-props/_config.js new file mode 100644 index 0000000000..d5ba1ef64b --- /dev/null +++ b/packages/svelte/test/runtime/samples/dynamic-component-spread-props/_config.js @@ -0,0 +1,26 @@ +export default { + html: ` +

value(1) = 1

+ + `, + + async test({ assert, window, target }) { + const button = target.querySelector('button'); + await button.dispatchEvent(new window.Event('click')); + assert.htmlEqual( + target.innerHTML, + ` +

value(2) = 2

+ + ` + ); + await button.dispatchEvent(new window.Event('click')); + assert.htmlEqual( + target.innerHTML, + ` +

value(1) = 1

+ + ` + ); + } +}; diff --git a/packages/svelte/test/runtime/samples/dynamic-component-spread-props/main.svelte b/packages/svelte/test/runtime/samples/dynamic-component-spread-props/main.svelte new file mode 100644 index 0000000000..b8c45c83a0 --- /dev/null +++ b/packages/svelte/test/runtime/samples/dynamic-component-spread-props/main.svelte @@ -0,0 +1,12 @@ + + + + +