Merge branch 'master' into feat/copy-code-button

pull/9191/head
Puru Vijay 3 years ago
commit 6798c62d5c

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: ensure `svelte:component` evaluates props once

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: handle destructured primitive literals

@ -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?

@ -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 `<svelte:options>` 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 `<svelte:options>` 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: "<desired name>"`.
- `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: "<desired name>"`.
- `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
<svelte:options
@ -70,12 +85,35 @@ When constructing a custom element, you can tailor several aspects by defining `
shadow: 'none',
props: {
name: { reflect: true, type: 'Number', attribute: 'element-index' }
},
extend: (customElementConstructor) => {
// 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();
}
};
}
}}
/>
<script>
export let elementIndex;
export let attachedInternals;
// ...
function check() {
attachedInternals.checkValidity();
}
</script>
...
@ -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 `<slot>` element is inside an `{#if ...}` block. Similarly, including a `<slot>` 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.

@ -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() {

@ -48,7 +48,7 @@
{#if selected}
{#await selected then d}
<div class="photo" in:receive={{ key: d.id }} out:send={{ key: d.id }}>
<div class="photo" in:receive|global={{ key: d.id }} out:send|global={{ key: d.id }}>
<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-noninteractive-element-interactions -->
<img alt={d.alt} src="{ASSETS}/{d.id}.jpg" on:click={() => (selected = null)} />

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

@ -1676,6 +1676,9 @@ export interface SvelteHTMLElements {
}
>
| undefined;
extend?: (
svelteCustomElementClass: new () => HTMLElement
) => new () => HTMLElement | undefined;
};
immutable?: boolean | undefined;
accessors?: boolean | undefined;

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

@ -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<string,{attribute?:string;reflect?:boolean;type?:'String'|'Boolean'|'Number'|'Array'|'Object';}>} [customElement.props]
* @property {(ceClass: new () => HTMLElement) => new () => HTMLElement} [customElement.extend]
*/

@ -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"]);
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"]);

@ -54,7 +54,8 @@ export default class Expression {
/** @type {Array<import('estree').Node | import('estree').Node[]>} */
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<string>} */

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

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

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

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

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

@ -0,0 +1,22 @@
<svelte:options
customElement={{
tag: 'custom-element',
extend: (CeClass) => {
return class extends CeClass {
updateFoo(value) {
this.foo = value;
}
};
}
}}
/>
<script>
export function updateFoo(value) {
foo = value;
}
export let foo;
</script>
<p>{foo}</p>

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

@ -0,0 +1,6 @@
<script>
const tasks = ["do laundry", "do taxes", "cook food", "watch the kids"];
</script>
<slot {tasks} />

@ -0,0 +1,25 @@
export default {
html: `
<select>
<option value='do laundry'>do laundry</option>
<option value='do taxes'>do taxes</option>
<option value='cook food'>cook food</option>
<option value='watch the kids'>watch the kids</option>
</select>
<p>1</p>
`,
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);
}
};

@ -0,0 +1,19 @@
<script>
import Parent from "./Parent.svelte";
export let selected;
export let tasks = ['do nothing'];
export let tasks_touched = 0;
$: {
tasks, tasks_touched++;
}
</script>
<Parent let:tasks={tasks}>
<select bind:value={selected}>
{#each tasks as task}
<option value={task}>{task}</option>
{/each}
</select>
</Parent>
<p>{tasks_touched}</p>

@ -0,0 +1,6 @@
<script>
const tasks = ["do laundry", "do taxes", "cook food", "watch the kids"];
</script>
<slot {tasks} />

@ -0,0 +1,21 @@
export default {
html: `
<select>
<option value='do laundry'>do laundry</option>
<option value='do taxes'>do taxes</option>
<option value='cook food'>cook food</option>
<option value='watch the kids'>watch the kids</option>
</select>
`,
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);
}
};

@ -0,0 +1,12 @@
<script>
import Parent from "./Parent.svelte";
export let selected;
</script>
<Parent let:tasks={tasks}>
<select bind:value={selected}>
{#each tasks as task}
<option value={task}>{task}</option>
{/each}
</select>
</Parent>

@ -0,0 +1,5 @@
<script>
export let value;
</script>
<p>value(1) = {value}</p>

@ -0,0 +1,5 @@
<script>
export let value;
</script>
<p>value(2) = {value}</p>

@ -0,0 +1,26 @@
export default {
html: `
<p>value(1) = 1</p>
<button>Toggle Component</button>
`,
async test({ assert, window, target }) {
const button = target.querySelector('button');
await button.dispatchEvent(new window.Event('click'));
assert.htmlEqual(
target.innerHTML,
`
<p>value(2) = 2</p>
<button>Toggle Component</button>
`
);
await button.dispatchEvent(new window.Event('click'));
assert.htmlEqual(
target.innerHTML,
`
<p>value(1) = 1</p>
<button>Toggle Component</button>
`
);
}
};

@ -0,0 +1,12 @@
<script>
import Comp1 from './Comp1.svelte';
import Comp2 from './Comp2.svelte';
let view = Comp1;
$: props = view === Comp1 ? { value: 1 } : { value: 2 };
</script>
<svelte:component this={view} {...props} />
<button on:click={(e) => (view = view === Comp1 ? Comp2 : Comp1)}>Toggle Component</button>
Loading…
Cancel
Save