Merge branch 'master' into chore/npm-audit-fix

pull/7421/head
tarunama 4 years ago committed by GitHub
commit 80de680c0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,5 +1,16 @@
# Svelte changelog # Svelte changelog
## Unreleased
* Return the context object in `setContext` [#7427](https://github.com/sveltejs/svelte/issues/7427)
## 3.47.0
* Add support for dynamic elements through `<svelte:element>` ([#2324](https://github.com/sveltejs/svelte/issues/2324))
* Miscellaneous variable context fixes in `{@const}` ([#7222](https://github.com/sveltejs/svelte/pull/7222))
* Fix `{#key}` block not being reactive when the key variable is not otherwise used ([#7408](https://github.com/sveltejs/svelte/issues/7408))
* Add `Symbol` as a known global ([#7418](https://github.com/sveltejs/svelte/issues/7418))
## 3.46.6 ## 3.46.6
* Actually include action TypeScript interface in published package ([#7407](https://github.com/sveltejs/svelte/pull/7407)) * Actually include action TypeScript interface in published package ([#7407](https://github.com/sveltejs/svelte/pull/7407))

5
package-lock.json generated

@ -1,12 +1,11 @@
{ {
"name": "svelte", "name": "svelte",
"version": "3.46.6", "version": "3.47.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "svelte", "version": "3.47.0",
"version": "3.46.6",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@ampproject/remapping": "^0.3.0", "@ampproject/remapping": "^0.3.0",

@ -1,6 +1,6 @@
{ {
"name": "svelte", "name": "svelte",
"version": "3.46.6", "version": "3.47.0",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"module": "index.mjs", "module": "index.mjs",
"main": "index", "main": "index",

@ -12,7 +12,9 @@ Because of that, getting started with it can be a little bit confusing at first.
## 1. Use the REPL ## 1. Use the REPL
The [Svelte REPL](/repl) is the easiest way to begin. You can choose from a list of examples to get you started, and tweak them until they do what you want. The [Svelte REPL](/repl) (Read-Eval-Print Loop) is the easiest way to begin. This is an interactive environment that allows you to modify code and instantly see the result.
You can choose from a list of [examples](/examples/), click the [REPL](/repl) link, and then tweak them until they do what you want.
<aside><p>You'll need to have <a href="https://nodejs.org/">Node.js</a> installed, and know how to use the terminal</p></aside> <aside><p>You'll need to have <a href="https://nodejs.org/">Node.js</a> installed, and know how to use the terminal</p></aside>

@ -1627,6 +1627,28 @@ If `this` is falsy, no component is rendered.
<svelte:component this={currentSelection.component} foo={bar}/> <svelte:component this={currentSelection.component} foo={bar}/>
``` ```
### `<svelte:element>`
```sv
<svelte:element this={expression}/>
```
---
The `<svelte:element>` element lets you render an element of a dynamically specified type. This is useful for example when rich text content from a CMS. If the tag is changed, the children will be preserved unless there's a transition attached to the element. Any properties and event listeners present will be applied to the element.
The only supported binding is `bind:this`, since the element type specific bindings that Svelte does at build time (e.g. `bind:value` for input elements) does not work with a dynamic tag type.
If `this` has a nullish value, a warning will be logged in development mode.
```sv
<script>
let tag = 'div';
export let handler;
</script>
<svelte:element this={tag} on:click={handler}>Foo</svelte:element>
```
### `<svelte:window>` ### `<svelte:window>`
@ -1683,7 +1705,7 @@ All except `scrollX` and `scrollY` are readonly.
Similarly to `<svelte:window>`, this element allows you to add listeners to events on `document.body`, such as `mouseenter` and `mouseleave`, which don't fire on `window`. It also lets you use [actions](/docs#template-syntax-element-directives-use-action) on the `<body>` element. Similarly to `<svelte:window>`, this element allows you to add listeners to events on `document.body`, such as `mouseenter` and `mouseleave`, which don't fire on `window`. It also lets you use [actions](/docs#template-syntax-element-directives-use-action) on the `<body>` element.
`<svelte:body>` also has to appear at the top level of your component. As with `<svelte:window>`, this element may only appear the top level of your component and must never be inside a block or element.
```sv ```sv
<svelte:body <svelte:body
@ -1704,7 +1726,7 @@ Similarly to `<svelte:window>`, this element allows you to add listeners to even
This element makes it possible to insert elements into `document.head`. During server-side rendering, `head` content is exposed separately to the main `html` content. This element makes it possible to insert elements into `document.head`. During server-side rendering, `head` content is exposed separately to the main `html` content.
As with `<svelte:window>` and `<svelte:body>`, this element has to appear at the top level of your component and cannot be inside a block or other element. As with `<svelte:window>` and `<svelte:body>`, this element may only appear at the top level of your component and must never be inside a block or element.
```sv ```sv
<svelte:head> <svelte:head>

@ -148,7 +148,7 @@ setContext(key: any, context: any)
--- ---
Associates an arbitrary `context` object with the current component and the specified `key`. The context is then available to children of the component (including slotted content) with `getContext`. Associates an arbitrary `context` object with the current component and the specified `key` and returns that object. The context is then available to children of the component (including slotted content) with `getContext`.
Like lifecycle functions, this must be called during component initialisation. Like lifecycle functions, this must be called during component initialisation.

@ -2,11 +2,7 @@
title: Compile time title: Compile time
--- ---
Typically, you won't interact with the Svelte compiler directly, but will instead integrate it into your build system using a bundler plugin: Typically, you won't interact with the Svelte compiler directly, but will instead integrate it into your build system using a bundler plugin. The bundler plugin that the Svelte team most recommends and invests in is [vite-plugin-svelte](https://github.com/sveltejs/vite-plugin-svelte). The [SvelteKit](https://kit.svelte.dev/) framework provides a setup leveraging `vite-plugin-svelte` to build applications as well as a [tool for packaging Svelte component libraries](https://kit.svelte.dev/docs/packaging). Svelte Society maintains a list of [other bundler plugins](https://sveltesociety.dev/tools/#bundling) for additional tools like Rollup and Webpack.
* [rollup-plugin-svelte](https://github.com/sveltejs/rollup-plugin-svelte) for users of [Rollup](https://rollupjs.org)
* [svelte-loader](https://github.com/sveltejs/svelte-loader) for users of [webpack](https://webpack.js.org)
* or one of the [community-maintained plugins](https://sveltesociety.dev/tools)
Nonetheless, it's useful to understand how to use the compiler, since bundler plugins generally expose compiler options to you. Nonetheless, it's useful to understand how to use the compiler, since bundler plugins generally expose compiler options to you.
@ -77,7 +73,7 @@ The following options can be passed to the compiler. None are required:
| `accessors` | `false` | If `true`, getters and setters will be created for the component's props. If `false`, they will only be created for readonly exported values (i.e. those declared with `const`, `class` and `function`). If compiling with `customElement: true` this option defaults to `true`. | `accessors` | `false` | If `true`, getters and setters will be created for the component's props. If `false`, they will only be created for readonly exported values (i.e. those declared with `const`, `class` and `function`). If compiling with `customElement: true` this option defaults to `true`.
| `customElement` | `false` | If `true`, tells the compiler to generate a custom element constructor instead of a regular Svelte component. | `customElement` | `false` | If `true`, tells the compiler to generate a custom element constructor instead of a regular Svelte component.
| `tag` | `null` | A `string` that tells Svelte what tag name to register the custom element with. It must be a lowercase alphanumeric string with at least one hyphen, e.g. `"my-element"`. | `tag` | `null` | A `string` that tells Svelte what tag name to register the custom element with. It must be a lowercase alphanumeric string with at least one hyphen, e.g. `"my-element"`.
| `css` | `true` | If `true`, styles will be included in the JavaScript class and injected at runtime. It's recommended that you set this to `false` and use the CSS that is statically generated, as it will result in smaller JavaScript bundles and better performance. | `css` | `true` | If `true`, styles will be included in the JavaScript class and injected at runtime for the components actually rendered. If `false`, the CSS will be returned in the `css` field of the compilation result. Most Svelte bundler plugins will set this to `false` and use the CSS that is statically generated for better performance, as it will result in smaller JavaScript bundles and the output can be served as cacheable `.css` files.
| `cssHash` | See right | A function that takes a `{ hash, css, name, filename }` argument and returns the string that is used as a classname for scoped CSS. It defaults to returning `svelte-${hash(css)}` | `cssHash` | See right | A function that takes a `{ hash, css, name, filename }` argument and returns the string that is used as a classname for scoped CSS. It defaults to returning `svelte-${hash(css)}`
| `loopGuardTimeout` | 0 | A `number` that tells Svelte to break the loop if it blocks the thread for more than `loopGuardTimeout` ms. This is useful to prevent infinite loops. **Only available when `dev: true`** | `loopGuardTimeout` | 0 | A `number` that tells Svelte to break the loop if it blocks the thread for more than `loopGuardTimeout` ms. This is useful to prevent infinite loops. **Only available when `dev: true`**
| `preserveComments` | `false` | If `true`, your HTML comments will be preserved during server-side rendering. By default, they are stripped out. | `preserveComments` | `false` | If `true`, your HTML comments will be preserved during server-side rendering. By default, they are stripped out.

@ -0,0 +1,12 @@
<script>
const options = ['h1', 'h3', 'p'];
let selected = options[0];
</script>
<select bind:value={selected}>
{#each options as option}
<option value={option}>{option}</option>
{/each}
</select>
<svelte:element this={selected}>I'm a {selected} tag</svelte:element>

@ -8,4 +8,5 @@ There are a few approaches that people take when testing, but it generally invol
Some resources for getting started with unit testing: Some resources for getting started with unit testing:
- [Svelte Testing Library](https://testing-library.com/docs/svelte-testing-library/example/) - [Svelte Testing Library](https://testing-library.com/docs/svelte-testing-library/example/)
- [Example using vitest](https://github.com/vitest-dev/vitest/tree/main/examples/svelte)
- [Example using uvu test runner with JSDOM](https://github.com/lukeed/uvu/tree/master/examples/svelte) - [Example using uvu test runner with JSDOM](https://github.com/lukeed/uvu/tree/master/examples/svelte)

@ -4,7 +4,7 @@ title: Keyed each blocks
By default, when you modify the value of an `each` block, it will add and remove items at the *end* of the block, and update any values that have changed. That might not be what you want. By default, when you modify the value of an `each` block, it will add and remove items at the *end* of the block, and update any values that have changed. That might not be what you want.
It's easier to show why than to explain. Click the 'Remove first thing' button a few times, and notice what happens: It removes the first `<Thing>` component, but the *last* DOM node. Then it updates the `name` value in the remaining DOM nodes, but not the emoji. It's easier to show why than to explain. Click the 'Remove first thing' button a few times, and notice what happens: it does not remove the first `<Thing>` component, but rather the *last* DOM node. Then it updates the `name` value in the remaining DOM nodes, but not the emoji.
Instead, we'd like to remove only the first `<Thing>` component and its DOM node, and leave the others unaffected. Instead, we'd like to remove only the first `<Thing>` component and its DOM node, and leave the others unaffected.

@ -10,4 +10,4 @@ We can also use `bind:value` with `<select>` elements. Update line 20:
Note that the `<option>` values are objects rather than strings. Svelte doesn't mind. Note that the `<option>` values are objects rather than strings. Svelte doesn't mind.
> Because we haven't set an initial value of `selected`, the binding will set it to the default value (the first in the list) automatically. Be careful though — until the binding is initialised, `selected` remains undefined, so we can't blindly reference e.g. `selected.id` in the template. > Because we haven't set an initial value of `selected`, the binding will set it to the default value (the first in the list) automatically. Be careful though — until the binding is initialised, `selected` remains undefined, so we can't blindly reference e.g. `selected.id` in the template. If your use case allows it, you could also set an initial value to bypass this problem.

@ -0,0 +1,18 @@
<script>
const options = ['h1', 'h3', 'p'];
let selected = options[0];
</script>
<select bind:value={selected}>
{#each options as option}
<option value={option}>{option}</option>
{/each}
</select>
{#if selected === 'h1'}
<h1>I'm a h1 tag</h1>
{:else if selected === 'h3'}
<h3>I'm a h3 tag</h3>
{:else if selected === 'p'}
<p>I'm a p tag</p>
{/if}

@ -0,0 +1,12 @@
<script>
const options = ['h1', 'h3', 'p'];
let selected = options[0];
</script>
<select bind:value={selected}>
{#each options as option}
<option value={option}>{option}</option>
{/each}
</select>
<svelte:element this={selected}>I'm a {selected} tag</svelte:element>

@ -0,0 +1,23 @@
---
title: <svelte:element>
---
Sometimes we don't know in advance what kind of DOM element to render. `<svelte:element>` comes in handy here. Instead of a sequence of `if` blocks...
```html
{#if selected === 'h1'}
<h1>I'm a h1 tag</h1>
{:else if selected === 'h3'}
<h3>I'm a h3 tag</h3>
{:else if selected === 'p'}
<p>I'm a p tag</p>
{/if}
```
...we can have a single dynamic component:
```html
<svelte:element this={selected}>I'm a {selected} tag</svelte:element>
```
The `this` value can be any string, or a falsy value — if it's falsy, no element is rendered.

@ -246,6 +246,10 @@ export default {
code: 'invalid-animation', code: 'invalid-animation',
message: 'An element that uses the animate directive must be the sole child of a keyed each block' message: 'An element that uses the animate directive must be the sole child of a keyed each block'
}, },
invalid_animation_dynamic_element: {
code: 'invalid-animation',
message: '<svelte:element> cannot have a animate directive'
},
invalid_directive_value: { invalid_directive_value: {
code: 'invalid-directive-value', code: 'invalid-directive-value',
message: 'Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)' message: 'Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)'

@ -18,6 +18,9 @@ import Let from './Let';
import TemplateScope from './shared/TemplateScope'; import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces'; import { INode } from './interfaces';
import Component from '../Component'; import Component from '../Component';
import Expression from './shared/Expression';
import { string_literal } from '../utils/stringify';
import { Literal } from 'estree';
import compiler_warnings from '../compiler_warnings'; import compiler_warnings from '../compiler_warnings';
import compiler_errors from '../compiler_errors'; import compiler_errors from '../compiler_errors';
@ -190,11 +193,26 @@ export default class Element extends Node {
children: INode[]; children: INode[];
namespace: string; namespace: string;
needs_manual_style_scoping: boolean; needs_manual_style_scoping: boolean;
tag_expr: Expression;
get is_dynamic_element() {
return this.name === 'svelte:element';
}
constructor(component: Component, parent: Node, scope: TemplateScope, info: any) { constructor(component: Component, parent: Node, scope: TemplateScope, info: any) {
super(component, parent, scope, info); super(component, parent, scope, info);
this.name = info.name; this.name = info.name;
if (info.name === 'svelte:element') {
if (typeof info.tag !== 'string') {
this.tag_expr = new Expression(component, this, scope, info.tag);
} else {
this.tag_expr = new Expression(component, this, scope, string_literal(info.tag) as Literal);
}
} else {
this.tag_expr = new Expression(component, this, scope, string_literal(this.name) as Literal);
}
this.namespace = get_namespace(parent as Element, this, component.namespace); this.namespace = get_namespace(parent as Element, this, component.namespace);
if (this.namespace !== namespaces.foreign) { if (this.namespace !== namespaces.foreign) {

@ -254,11 +254,17 @@ export default class Expression {
const declaration = b`const ${id} = ${node}`; const declaration = b`const ${id} = ${node}`;
if (owner.type === 'ConstTag') { if (owner.type === 'ConstTag') {
let child_scope = scope;
walk(node, { walk(node, {
enter(node: Node) { enter(node: Node, parent: any) {
if (node.type === 'Identifier') { if (map.has(node)) child_scope = map.get(node);
if (node.type === 'Identifier' && is_reference(node, parent)) {
if (child_scope.has(node.name)) return;
this.replace(block.renderer.reference(node, ctx)); this.replace(block.renderer.reference(node, ctx));
} }
},
leave(node: Node) {
if (map.has(node)) child_scope = child_scope.parent;
} }
}); });
} else if (dependencies.size === 0 && contextual_dependencies.size === 0) { } else if (dependencies.size === 0 && contextual_dependencies.size === 0) {

@ -48,6 +48,7 @@ export default class Block {
hydrate: Array<Node | Node[]>; hydrate: Array<Node | Node[]>;
mount: Array<Node | Node[]>; mount: Array<Node | Node[]>;
measure: Array<Node | Node[]>; measure: Array<Node | Node[]>;
restore_measurements: Array<Node | Node[]>;
fix: Array<Node | Node[]>; fix: Array<Node | Node[]>;
animate: Array<Node | Node[]>; animate: Array<Node | Node[]>;
intro: Array<Node | Node[]>; intro: Array<Node | Node[]>;
@ -96,6 +97,7 @@ export default class Block {
hydrate: [], hydrate: [],
mount: [], mount: [],
measure: [], measure: [],
restore_measurements: [],
fix: [], fix: [],
animate: [], animate: [],
intro: [], intro: [],
@ -326,6 +328,12 @@ export default class Block {
${this.chunks.measure} ${this.chunks.measure}
}`; }`;
if (this.chunks.restore_measurements.length) {
properties.restore_measurements = x`function #restore_measurements(#measurement) {
${this.chunks.restore_measurements}
}`;
}
properties.fix = x`function #fix() { properties.fix = x`function #fix() {
${this.chunks.fix} ${this.chunks.fix}
}`; }`;
@ -379,6 +387,7 @@ export default class Block {
m: ${properties.mount}, m: ${properties.mount},
p: ${properties.update}, p: ${properties.update},
r: ${properties.measure}, r: ${properties.measure},
s: ${properties.restore_measurements},
f: ${properties.fix}, f: ${properties.fix},
a: ${properties.animate}, a: ${properties.animate},
i: ${properties.intro}, i: ${properties.intro},

@ -26,6 +26,7 @@ import Action from '../../../nodes/Action';
import MustacheTagWrapper from '../MustacheTag'; import MustacheTagWrapper from '../MustacheTag';
import RawMustacheTagWrapper from '../RawMustacheTag'; import RawMustacheTagWrapper from '../RawMustacheTag';
import is_dynamic from '../shared/is_dynamic'; import is_dynamic from '../shared/is_dynamic';
import create_debugging_comment from '../shared/create_debugging_comment';
import { push_array } from '../../../../utils/push_array'; import { push_array } from '../../../../utils/push_array';
interface BindingGroup { interface BindingGroup {
@ -134,6 +135,8 @@ const events = [
} }
]; ];
const CHILD_DYNAMIC_ELEMENT_BLOCK = 'child_dynamic_element';
export default class ElementWrapper extends Wrapper { export default class ElementWrapper extends Wrapper {
node: Element; node: Element;
fragment: FragmentWrapper; fragment: FragmentWrapper;
@ -147,6 +150,9 @@ export default class ElementWrapper extends Wrapper {
var: any; var: any;
void: boolean; void: boolean;
child_dynamic_element_block?: Block = null;
child_dynamic_element?: ElementWrapper = null;
constructor( constructor(
renderer: Renderer, renderer: Renderer,
block: Block, block: Block,
@ -156,6 +162,24 @@ export default class ElementWrapper extends Wrapper {
next_sibling: Wrapper next_sibling: Wrapper
) { ) {
super(renderer, block, parent, node); super(renderer, block, parent, node);
if (node.is_dynamic_element && block.type !== CHILD_DYNAMIC_ELEMENT_BLOCK) {
this.child_dynamic_element_block = block.child({
comment: create_debugging_comment(node, renderer.component),
name: renderer.component.get_unique_name('create_dynamic_element'),
type: CHILD_DYNAMIC_ELEMENT_BLOCK
});
renderer.blocks.push(this.child_dynamic_element_block);
this.child_dynamic_element = new ElementWrapper(
renderer,
this.child_dynamic_element_block,
parent,
node,
strip_whitespace,
next_sibling
);
}
this.var = { this.var = {
type: 'Identifier', type: 'Identifier',
name: node.name.replace(/[^a-zA-Z0-9_$]/g, '_') name: node.name.replace(/[^a-zA-Z0-9_$]/g, '_')
@ -199,6 +223,8 @@ export default class ElementWrapper extends Wrapper {
block.add_animation(); block.add_animation();
} }
block.add_dependencies(node.tag_expr.dependencies);
// add directive and handler dependencies // add directive and handler dependencies
[node.animation, node.outro, ...node.actions, ...node.classes, ...node.styles].forEach(directive => { [node.animation, node.outro, ...node.actions, ...node.classes, ...node.styles].forEach(directive => {
if (directive && directive.expression) { if (directive && directive.expression) {
@ -221,6 +247,7 @@ export default class ElementWrapper extends Wrapper {
node.handlers.length > 0 || node.handlers.length > 0 ||
node.styles.length > 0 || node.styles.length > 0 ||
this.node.name === 'option' || this.node.name === 'option' ||
node.tag_expr.dynamic_dependencies().length ||
renderer.options.dev renderer.options.dev
) { ) {
this.parent.cannot_use_innerhtml(); // need to use add_location this.parent.cannot_use_innerhtml(); // need to use add_location
@ -232,6 +259,110 @@ export default class ElementWrapper extends Wrapper {
} }
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) { render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
if (this.child_dynamic_element) {
this.render_dynamic_element(block, parent_node, parent_nodes);
} else {
this.render_element(block, parent_node, parent_nodes);
}
}
render_dynamic_element(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
this.child_dynamic_element.render(
this.child_dynamic_element_block,
null,
(x`#nodes` as unknown) as Identifier
);
const previous_tag = block.get_unique_name('previous_tag');
const tag = this.node.tag_expr.manipulate(block);
block.add_variable(previous_tag, tag);
block.chunks.init.push(b`
${this.renderer.options.dev && b`@validate_dynamic_element(${tag});`}
let ${this.var} = ${tag} && ${this.child_dynamic_element_block.name}(#ctx);
`);
block.chunks.create.push(b`
if (${this.var}) ${this.var}.c();
`);
if (this.renderer.options.hydratable) {
block.chunks.claim.push(b`
if (${this.var}) ${this.var}.l(${parent_nodes});
`);
}
block.chunks.mount.push(b`
if (${this.var}) ${this.var}.m(${parent_node || '#target'}, ${parent_node ? 'null' : '#anchor'});
`);
const anchor = this.get_or_create_anchor(block, parent_node, parent_nodes);
const has_transitions = !!(this.node.intro || this.node.outro);
const not_equal = this.renderer.component.component_options.immutable ? x`@not_equal` : x`@safe_not_equal`;
block.chunks.update.push(b`
if (${tag}) {
if (!${previous_tag}) {
${this.var} = ${this.child_dynamic_element_block.name}(#ctx);
${this.var}.c();
${has_transitions && b`@transition_in(${this.var})`}
${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor});
} else if (${not_equal}(${previous_tag}, ${tag})) {
${this.var}.d(1);
${this.renderer.options.dev && b`@validate_dynamic_element(${tag});`}
${this.var} = ${this.child_dynamic_element_block.name}(#ctx);
${this.var}.c();
${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor});
} else {
${this.var}.p(#ctx, #dirty);
}
} else if (${previous_tag}) {
${
has_transitions
? b`
@group_outros();
@transition_out(${this.var}, 1, 1, () => {
${this.var} = null;
});
@check_outros();
`
: b`
${this.var}.d(1);
${this.var} = null;
`
}
}
${previous_tag} = ${tag};
`);
if (this.child_dynamic_element_block.has_intros) {
block.chunks.intro.push(b`@transition_in(${this.var});`);
}
if (this.child_dynamic_element_block.has_outros) {
block.chunks.outro.push(b`@transition_out(${this.var});`);
}
block.chunks.destroy.push(b`if (${this.var}) ${this.var}.d(detaching)`);
if (this.node.animation) {
const measurements = block.get_unique_name('measurements');
block.add_variable(measurements);
block.chunks.measure.push(b`${measurements} = ${this.var}.r()`);
block.chunks.fix.push(b`${this.var}.f();`);
block.chunks.animate.push(b`
${this.var}.s(${measurements});
${this.var}.a()
`);
}
}
is_dom_node() {
return super.is_dom_node() && !this.child_dynamic_element;
}
render_element(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
const { renderer } = this; const { renderer } = this;
if (this.node.name === 'noscript') return; if (this.node.name === 'noscript') return;
@ -249,7 +380,7 @@ export default class ElementWrapper extends Wrapper {
if (renderer.options.hydratable) { if (renderer.options.hydratable) {
if (parent_nodes) { if (parent_nodes) {
block.chunks.claim.push(b` block.chunks.claim.push(b`
${node} = ${this.get_claim_statement(parent_nodes)}; ${node} = ${this.get_claim_statement(block, parent_nodes)};
`); `);
if (!this.void && this.node.children.length > 0) { if (!this.void && this.node.children.length > 0) {
@ -357,6 +488,8 @@ export default class ElementWrapper extends Wrapper {
b`@add_location(${this.var}, ${renderer.file_var}, ${loc.line - 1}, ${loc.column}, ${this.node.start});` b`@add_location(${this.var}, ${renderer.file_var}, ${loc.line - 1}, ${loc.column}, ${this.node.start});`
); );
} }
block.renderer.dirty(this.node.tag_expr.dynamic_dependencies());
} }
can_use_textcontent() { can_use_textcontent() {
@ -364,7 +497,7 @@ export default class ElementWrapper extends Wrapper {
} }
get_render_statement(block: Block) { get_render_statement(block: Block) {
const { name, namespace } = this.node; const { name, namespace, tag_expr } = this.node;
if (namespace === namespaces.svg) { if (namespace === namespaces.svg) {
return x`@svg_element("${name}")`; return x`@svg_element("${name}")`;
@ -379,22 +512,32 @@ export default class ElementWrapper extends Wrapper {
return x`@element_is("${name}", ${is.render_chunks(block).reduce((lhs, rhs) => x`${lhs} + ${rhs}`)})`; return x`@element_is("${name}", ${is.render_chunks(block).reduce((lhs, rhs) => x`${lhs} + ${rhs}`)})`;
} }
return x`@element("${name}")`; const reference = tag_expr.manipulate(block);
return x`@element(${reference})`;
} }
get_claim_statement(nodes: Identifier) { get_claim_statement(block: Block, nodes: Identifier) {
const attributes = this.attributes const attributes = this.attributes
.filter((attr) => !(attr instanceof SpreadAttributeWrapper) && !attr.property_name) .filter((attr) => !(attr instanceof SpreadAttributeWrapper) && !attr.property_name)
.map((attr) => p`${(attr as StyleAttributeWrapper | AttributeWrapper).name}: true`); .map((attr) => p`${(attr as StyleAttributeWrapper | AttributeWrapper).name}: true`);
const name = this.node.namespace let reference;
? this.node.name if (this.node.tag_expr.node.type === 'Literal') {
: this.node.name.toUpperCase(); if (this.node.namespace) {
reference = `"${this.node.tag_expr.node.value}"`;
} else {
reference = `"${(this.node.tag_expr.node.value as String || '').toUpperCase()}"`;
}
} else if (this.node.namespace) {
reference = x`${this.node.tag_expr.manipulate(block)}`;
} else {
reference = x`(${this.node.tag_expr.manipulate(block)} || 'null').toUpperCase()`;
}
if (this.node.namespace === namespaces.svg) { if (this.node.namespace === namespaces.svg) {
return x`@claim_svg_element(${nodes}, "${name}", { ${attributes} })`; return x`@claim_svg_element(${nodes}, ${reference}, { ${attributes} })`;
} else { } else {
return x`@claim_element(${nodes}, "${name}", { ${attributes} })`; return x`@claim_element(${nodes}, ${reference}, { ${attributes} })`;
} }
} }
@ -847,6 +990,11 @@ export default class ElementWrapper extends Wrapper {
${rect} = ${this.var}.getBoundingClientRect(); ${rect} = ${this.var}.getBoundingClientRect();
`); `);
if (block.type === CHILD_DYNAMIC_ELEMENT_BLOCK) {
block.chunks.measure.push(b`return ${rect}`);
block.chunks.restore_measurements.push(b`${rect} = #measurement;`);
}
block.chunks.fix.push(b` block.chunks.fix.push(b`
@fix_position(${this.var}); @fix_position(${this.var});
${stop_animation}(); ${stop_animation}();
@ -940,7 +1088,7 @@ export default class ElementWrapper extends Wrapper {
if (should_cache) { if (should_cache) {
block.chunks.update.push(b` block.chunks.update.push(b`
if (${block.renderer.dirty(dependencies)} && (${cached_snippet} !== (${cached_snippet} = ${snippet}))) { if (${block.renderer.dirty(dependencies)} && (${cached_snippet} !== (${cached_snippet} = ${snippet}))) {
${updater} ${updater}
} }
`); `);
} else { } else {

@ -36,6 +36,7 @@ export default class KeyBlockWrapper extends Wrapper {
name: renderer.component.get_unique_name('create_key_block'), name: renderer.component.get_unique_name('create_key_block'),
type: 'key' type: 'key'
}); });
block.add_dependencies(node.expression.dependencies);
renderer.blocks.push(block); renderer.blocks.push(block);
} }

@ -8,8 +8,9 @@ import Expression from '../../nodes/shared/Expression';
import remove_whitespace_children from './utils/remove_whitespace_children'; import remove_whitespace_children from './utils/remove_whitespace_children';
import fix_attribute_casing from '../../render_dom/wrappers/Element/fix_attribute_casing'; import fix_attribute_casing from '../../render_dom/wrappers/Element/fix_attribute_casing';
import { namespaces } from '../../../utils/namespaces'; import { namespaces } from '../../../utils/namespaces';
import { Expression as ESExpression } from 'estree';
export default function(node: Element, renderer: Renderer, options: RenderOptions) { export default function (node: Element, renderer: Renderer, options: RenderOptions) {
const children = remove_whitespace_children(node.children, node.next); const children = remove_whitespace_children(node.children, node.next);
@ -22,7 +23,8 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
node.attributes.some((attribute) => attribute.name === 'contenteditable') node.attributes.some((attribute) => attribute.name === 'contenteditable')
); );
renderer.add_string(`<${node.name}`); renderer.add_string('<');
add_tag_name();
const class_expression_list = node.classes.map(class_directive => { const class_expression_list = node.classes.map(class_directive => {
const { expression, name } = class_directive; const { expression, name } = class_directive;
@ -167,14 +169,25 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
renderer.add_expression(node_contents); renderer.add_expression(node_contents);
} }
if (!is_void(node.name)) { add_close_tag();
renderer.add_string(`</${node.name}>`);
}
} else { } else {
renderer.render(children, options); renderer.render(children, options);
add_close_tag();
}
function add_close_tag() {
if (!is_void(node.name)) { if (!is_void(node.name)) {
renderer.add_string(`</${node.name}>`); renderer.add_string('</');
add_tag_name();
renderer.add_string('>');
}
}
function add_tag_name() {
if (node.tag_expr.node.type === 'Literal') {
renderer.add_string(node.tag_expr.node.value as string);
} else {
renderer.add_expression(node.tag_expr.node as ESExpression);
} }
} }
} }

@ -99,6 +99,10 @@ export default {
code: `invalid-${slug}-content`, code: `invalid-${slug}-content`,
message: `<${name}> cannot have children` message: `<${name}> cannot have children`
}), }),
invalid_element_definition: {
code: 'invalid-element-definition',
message: 'Invalid element definition'
},
invalid_element_placement: (slug: string, name: string) => ({ invalid_element_placement: (slug: string, name: string) => ({
code: `invalid-${slug}-placement`, code: `invalid-${slug}-placement`,
message: `<${name}> tags cannot be inside elements or blocks` message: `<${name}> tags cannot be inside elements or blocks`
@ -161,6 +165,10 @@ export default {
code: 'missing-attribute-value', code: 'missing-attribute-value',
message: 'Expected value for the attribute' message: 'Expected value for the attribute'
}, },
missing_element_definition: {
code: 'missing-element-definition',
message: '<svelte:element> must have a \'this\' attribute'
},
unclosed_script: { unclosed_script: {
code: 'unclosed-script', code: 'unclosed-script',
message: '<script> must have a closing tag' message: '<script> must have a closing tag'

@ -19,7 +19,7 @@ const meta_tags = new Map([
['svelte:body', 'Body'] ['svelte:body', 'Body']
]); ]);
const valid_meta_tags = Array.from(meta_tags.keys()).concat('svelte:self', 'svelte:component', 'svelte:fragment'); const valid_meta_tags = Array.from(meta_tags.keys()).concat('svelte:self', 'svelte:component', 'svelte:fragment', 'svelte:element');
const specials = new Map([ const specials = new Map([
[ [
@ -41,6 +41,7 @@ const specials = new Map([
const SELF = /^svelte:self(?=[\s/>])/; const SELF = /^svelte:self(?=[\s/>])/;
const COMPONENT = /^svelte:component(?=[\s/>])/; const COMPONENT = /^svelte:component(?=[\s/>])/;
const SLOT = /^svelte:fragment(?=[\s/>])/; const SLOT = /^svelte:fragment(?=[\s/>])/;
const ELEMENT = /^svelte:element(?=[\s/>])/;
function parent_is_head(stack) { function parent_is_head(stack) {
let i = stack.length; let i = stack.length;
@ -84,7 +85,7 @@ export default function tag(parser: Parser) {
parser.current().children.length parser.current().children.length
) { ) {
parser.error( parser.error(
parser_errors.invalid_element_content(slug, name), parser_errors.invalid_element_content(slug, name),
parser.current().children[0].start parser.current().children[0].start
); );
} }
@ -169,7 +170,7 @@ export default function tag(parser: Parser) {
if (name === 'svelte:component') { if (name === 'svelte:component') {
const index = element.attributes.findIndex(attr => attr.type === 'Attribute' && attr.name === 'this'); const index = element.attributes.findIndex(attr => attr.type === 'Attribute' && attr.name === 'this');
if (!~index) { if (index === -1) {
parser.error(parser_errors.missing_component_definition, start); parser.error(parser_errors.missing_component_definition, start);
} }
@ -181,6 +182,19 @@ export default function tag(parser: Parser) {
element.expression = definition.value[0].expression; element.expression = definition.value[0].expression;
} }
if (name === 'svelte:element') {
const index = element.attributes.findIndex(attr => attr.type === 'Attribute' && attr.name === 'this');
if (index === -1) {
parser.error(parser_errors.missing_element_definition, start);
}
const definition = element.attributes.splice(index, 1)[0];
if (definition.value === true) {
parser.error(parser_errors.invalid_element_definition, definition.start);
}
element.tag = definition.value[0].data || definition.value[0].expression;
}
// special cases top-level <script> and <style> // special cases top-level <script> and <style>
if (specials.has(name) && parser.stack.length === 1) { if (specials.has(name) && parser.stack.length === 1) {
const special = specials.get(name); const special = specials.get(name);
@ -247,6 +261,7 @@ function read_tag_name(parser: Parser) {
} }
if (parser.read(COMPONENT)) return 'svelte:component'; if (parser.read(COMPONENT)) return 'svelte:component';
if (parser.read(ELEMENT)) return 'svelte:element';
if (parser.read(SLOT)) return 'svelte:fragment'; if (parser.read(SLOT)) return 'svelte:fragment';
@ -258,7 +273,7 @@ function read_tag_name(parser: Parser) {
const match = fuzzymatch(name.slice(7), valid_meta_tags); const match = fuzzymatch(name.slice(7), valid_meta_tags);
parser.error( parser.error(
parser_errors.invalid_tag_name_svelte_element(valid_meta_tags, match), parser_errors.invalid_tag_name_svelte_element(valid_meta_tags, match),
start start
); );
} }

@ -56,6 +56,7 @@ export const globals = new Set([
'setTimeout', 'setTimeout',
'String', 'String',
'SVGElement', 'SVGElement',
'Symbol',
'SyntaxError', 'SyntaxError',
'TypeError', 'TypeError',
'undefined', 'undefined',

@ -107,6 +107,12 @@ export function validate_slots(name, slot, keys) {
} }
} }
export function validate_dynamic_element(tag: unknown) {
if (tag && typeof tag !== 'string') {
throw new Error('<svelte:element> expects "this" attribute to be a string.');
}
}
type Props = Record<string, any>; type Props = Record<string, any>;
export interface SvelteComponentDev { export interface SvelteComponentDev {
$set(props?: Props): void; $set(props?: Props): void;

@ -46,8 +46,9 @@ export function createEventDispatcher<
}; };
} }
export function setContext<T>(key, context: T) { export function setContext<T>(key, context: T): T {
get_current_component().$$.context.set(key, context); get_current_component().$$.context.set(key, context);
return context;
} }
export function getContext<T>(key): T { export function getContext<T>(key): T {

@ -0,0 +1,21 @@
export default {
warnings: [
{
code: 'css-unused-selector',
end: {
character: 86,
column: 8,
line: 7
},
frame:
' 5: color: red;\n 6: }\n 7: .unused {\n ^\n 8: font-style: italic;\n 9: }',
message: 'Unused CSS selector ".unused"',
pos: 79,
start: {
character: 79,
column: 1,
line: 7
}
}
]
};

@ -0,0 +1 @@
.used.svelte-xyz{color:red}

@ -0,0 +1 @@
<div class="used svelte-xyz"></div>

@ -0,0 +1,10 @@
<svelte:element this="div" class="used" />
<style>
.used {
color: red;
}
.unused {
font-style: italic;
}
</style>

@ -15,4 +15,4 @@ const Component = create_ssr_component(($$result, $$props, $$bindings, slots) =>
<p>foo: ${escape(foo)}</p>`; <p>foo: ${escape(foo)}</p>`;
}); });
export default Component; export default Component;

@ -0,0 +1,2 @@
<svelte:element this="div"></svelte:element>
<svelte:element this="div" class="foo"></svelte:element>

@ -0,0 +1,50 @@
{
"html": {
"start": 0,
"end": 101,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 44,
"type": "Element",
"name": "svelte:element",
"attributes": [],
"children": [],
"tag": "div"
},
{
"type": "Text",
"start": 44,
"end": 45,
"data": "\n",
"raw": "\n"
},
{
"start": 45,
"end": 101,
"type": "Element",
"name": "svelte:element",
"attributes": [
{
"type": "Attribute",
"start": 72,
"end": 83,
"name": "class",
"value": [
{
"type": "Text",
"start": 79,
"end": 82,
"data": "foo",
"raw": "foo"
}
]
}
],
"children": [],
"tag": "div"
}
]
}
}

@ -0,0 +1,2 @@
<svelte:element this={tag}></svelte:element>
<svelte:element this={tag} class="foo"></svelte:element>

@ -0,0 +1,80 @@
{
"html": {
"start": 0,
"end": 101,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 44,
"type": "Element",
"name": "svelte:element",
"attributes": [],
"children": [],
"tag": {
"start": 22,
"end": 25,
"loc": {
"start": {
"line": 1,
"column": 22
},
"end": {
"line": 1,
"column": 25
}
},
"name": "tag",
"type": "Identifier"
}
},
{
"type": "Text",
"start": 44,
"end": 45,
"data": "\n",
"raw": "\n"
},
{
"start": 45,
"end": 101,
"type": "Element",
"name": "svelte:element",
"children": [],
"attributes": [
{
"type": "Attribute",
"start": 72,
"end": 83,
"name": "class",
"value": [
{
"type": "Text",
"start": 79,
"end": 82,
"data": "foo",
"raw": "foo"
}
]
}
],
"tag": {
"start": 67,
"end": 70,
"loc": {
"start": {
"line": 2,
"column": 22
},
"end": {
"line": 2,
"column": 25
}
},
"name": "tag",
"type": "Identifier"
}
}
]
}
}

@ -1,6 +1,6 @@
{ {
"code": "invalid-tag-name", "code": "invalid-tag-name",
"message": "Valid <svelte:...> tag names are svelte:head, svelte:options, svelte:window, svelte:body, svelte:self, svelte:component or svelte:fragment", "message": "Valid <svelte:...> tag names are svelte:head, svelte:options, svelte:window, svelte:body, svelte:self, svelte:component, svelte:fragment or svelte:element",
"pos": 10, "pos": 10,
"start": { "start": {
"character": 10, "character": 10,

@ -0,0 +1,29 @@
export default {
html: `
<p>0</p>
<p>bar: 1,2,3,1,1,2,3,2, num: 1</p>
<p>bar: 0,2,4,1,0,2,4,2, num: 2</p>
`,
async test({ component, target, assert }) {
assert.htmlEqual(
target.innerHTML,
`
<p>0</p>
<p>bar: 1,2,3,1,1,2,3,2, num: 1</p>
<p>bar: 0,2,4,1,0,2,4,2, num: 2</p>
`
);
component.nums = [1, 2, 3];
assert.htmlEqual(
target.innerHTML,
`
<p>0</p>
<p>bar: 1,2,3,1,1,2,3,2,1,2,3,3, num: 1</p>
<p>bar: 0,2,4,1,0,2,4,2,0,2,4,3, num: 2</p>
<p>bar: -100,0,100,1,-100,0,100,2,-100,0,100,3, num: 3</p>
`
);
}
};

@ -0,0 +1,26 @@
<script>
export let nums = [1, 2];
let foos = [
{
nums: [1, 2, 3],
},
{
nums: [0, 2, 4],
},
{
nums: [-100, 0, 100],
},
];
let foo = 0;
</script>
<p>{foo}</p>
{#each nums as num, index}
{@const bar = nums.map((num) => {
const func = (foos, num) => {
return [...foos.map((foo) => foo), num];
}
return func(foos[index].nums, num);
})}
<p>bar: {bar}, num: {num}</p>
{/each}

@ -0,0 +1,29 @@
export default {
html: `
<p>bar: 1,2,3,0,2,4,-100,0,100, num: 1</p>
<p>bar: 1,2,3,0,2,4,-100,0,100, num: 2</p>
<p>bar: 1,2,3,0,2,4,-100,0,100, num: 3</p>
`,
async test({ component, target, assert }) {
assert.htmlEqual(
target.innerHTML,
`
<p>bar: 1,2,3,0,2,4,-100,0,100, num: 1</p>
<p>bar: 1,2,3,0,2,4,-100,0,100, num: 2</p>
<p>bar: 1,2,3,0,2,4,-100,0,100, num: 3</p>
`
);
component.nums = [1, 2, 3, 4];
assert.htmlEqual(
target.innerHTML,
`
<p>bar: 1,2,3,0,2,4,-100,0,100, num: 1</p>
<p>bar: 1,2,3,0,2,4,-100,0,100, num: 2</p>
<p>bar: 1,2,3,0,2,4,-100,0,100, num: 3</p>
<p>bar: 1,2,3,0,2,4,-100,0,100, num: 4</p>
`
);
}
};

@ -0,0 +1,19 @@
<script>
export let nums = [1, 2, 3];
let foos = [
{
nums: [1, 2, 3],
},
{
nums: [0, 2, 4],
},
{
nums: [-100, 0, 100],
},
];
</script>
{#each nums as num}
{@const bar = foos.map((foos) => foos.nums)}
<p>bar: {bar}, num: {num}</p>
{/each}

@ -0,0 +1,32 @@
export default {
html: `
<p>foo: dummy-foo, num: dummy-num</p>
<p>bar: 1,2,3,2,, num: 1</p>
<p>bar: 1,2,3,2,, num: 2</p>
<p>bar: 1,2,3,2,, num: 3</p>
`,
async test({ component, target, assert }) {
assert.htmlEqual(
target.innerHTML,
`
<p>foo: dummy-foo, num: dummy-num</p>
<p>bar: 1,2,3,2,, num: 1</p>
<p>bar: 1,2,3,2,, num: 2</p>
<p>bar: 1,2,3,2,, num: 3</p>
`
);
component.nums = [1, 2, 3, 4];
assert.htmlEqual(
target.innerHTML,
`
<p>foo: dummy-foo, num: dummy-num</p>
<p>bar: 1,2,3,2,4,, num: 1</p>
<p>bar: 1,2,3,2,4,, num: 2</p>
<p>bar: 1,2,3,2,4,, num: 3</p>
<p>bar: 1,2,3,2,4,, num: 4</p>
`
);
}
};

@ -0,0 +1,33 @@
<script>
export let nums = [1, 2, 3];
let foos = [
{
nums: [1, 2, 3],
},
{
nums: [0, 2, 4],
},
{
nums: [-100, 0, 100],
},
];
let default_nums = [-1];
let foo = "dummy-foo";
let num = "dummy-num";
</script>
<p>foo: {foo}, num: {num}</p>
{#each nums as num}
{@const bar = foos.map((foo) =>
foo.nums.filter((num) => {
if (Object.keys($$slots).length) {
return false;
} else if (Object.keys(foo).length) {
return nums.includes(num) || default_nums.includes(num);
} else {
return false;
}
}) || num
)}
<p>bar: {bar}, num: {num}</p>
{/each}

@ -0,0 +1,29 @@
export default {
html: `
<p>0</p>
<p>bar: 1,2,3,1,1,2,3,2, num: 1</p>
<p>bar: 0,2,4,1,0,2,4,2, num: 2</p>
`,
async test({ component, target, assert }) {
assert.htmlEqual(
target.innerHTML,
`
<p>0</p>
<p>bar: 1,2,3,1,1,2,3,2, num: 1</p>
<p>bar: 0,2,4,1,0,2,4,2, num: 2</p>
`
);
component.nums = [1, 2, 3];
assert.htmlEqual(
target.innerHTML,
`
<p>0</p>
<p>bar: 1,2,3,1,1,2,3,2,1,2,3,3, num: 1</p>
<p>bar: 0,2,4,1,0,2,4,2,0,2,4,3, num: 2</p>
<p>bar: -100,0,100,1,-100,0,100,2,-100,0,100,3, num: 3</p>
`
);
}
};

@ -0,0 +1,25 @@
<script>
export let nums = [1, 2];
let foos = [
{
nums: [1, 2, 3],
},
{
nums: [0, 2, 4],
},
{
nums: [-100, 0, 100],
},
];
let foo = 0;
</script>
<p>{foo}</p>
{#each nums as num, index}
{@const bar = nums.map((num) => {
return (function (foos, num) {
return [...foos.map((foo) => foo), num];
})(foos[index].nums, num);
})}
<p>bar: {bar}, num: {num}</p>
{/each}

@ -0,0 +1,29 @@
export default {
html: `
<p>0</p>
<p>bar: 1,2,3,1,1,2,3,2, num: 1</p>
<p>bar: 0,2,4,1,0,2,4,2, num: 2</p>
`,
async test({ component, target, assert }) {
assert.htmlEqual(
target.innerHTML,
`
<p>0</p>
<p>bar: 1,2,3,1,1,2,3,2, num: 1</p>
<p>bar: 0,2,4,1,0,2,4,2, num: 2</p>
`
);
component.nums = [1, 2, 3];
assert.htmlEqual(
target.innerHTML,
`
<p>0</p>
<p>bar: 1,2,3,1,1,2,3,2,1,2,3,3, num: 1</p>
<p>bar: 0,2,4,1,0,2,4,2,0,2,4,3, num: 2</p>
<p>bar: -100,0,100,1,-100,0,100,2,-100,0,100,3, num: 3</p>
`
);
}
};

@ -0,0 +1,26 @@
<script>
export let nums = [1, 2];
let foos = [
{
nums: [1, 2, 3],
},
{
nums: [0, 2, 4],
},
{
nums: [-100, 0, 100],
},
];
let foo = 0;
</script>
<p>{foo}</p>
{#each nums as num, index}
{@const bar = nums.map((num) => {
function func(foos, num) {
return [...foos.map((foo) => foo), num];
}
return func(foos[index].nums, num);
})}
<p>bar: {bar}, num: {num}</p>
{/each}

@ -0,0 +1,37 @@
export default {
html: `
<p>1</p>
<p>3,6,9</p>
<p>2</p>
<p>3,6,9</p>
<p>3</p>
<p>3,6,9</p>
`,
test({ component, target, assert }) {
component.baz = 5;
assert.htmlEqual(
target.innerHTML,
`
<p>1</p>
<p>5,10,15</p>
<p>2</p>
<p>5,10,15</p>
<p>3</p>
<p>5,10,15</p>
`
);
component.array = [3, 4, 5];
assert.htmlEqual(
target.innerHTML,
`
<p>3</p>
<p>15,20,25</p>
<p>4</p>
<p>15,20,25</p>
<p>5</p>
<p>15,20,25</p>
`
);
}
};

@ -0,0 +1,15 @@
<script>
export let array = [1, 2, 3];
export let baz = 3;
const foo = (item) => item;
</script>
{#each array as item}
<p>{foo(item)}</p>
{@const bar = array.map((item) => {
const bar = baz;
const foo = (item) => item * bar;
return foo(item);
})}
<p>{bar}</p>
{/each}

@ -0,0 +1,5 @@
export default {
html: `
<div>true</div>
`
};

@ -0,0 +1,7 @@
<script>
import { setContext } from 'svelte';
const a = {};
const b = setContext('foo', a);
</script>
<div>{a === b}</div>

@ -0,0 +1,45 @@
let logs = [];
export default {
html: `
<h1>tag is h1.</h1>
`,
props: {
logs
},
after_test() {
logs = [];
},
async test({ assert, component, target }) {
assert.equal(component.tag, 'h1');
assert.deepEqual(logs, ['create: h1,opt1']);
component.opt = 'opt2';
assert.equal(component.tag, 'h1');
assert.deepEqual(logs, ['create: h1,opt1', 'update: h1,opt2']);
component.tag = 'h2';
assert.equal(component.tag, 'h2');
assert.deepEqual(logs, [
'create: h1,opt1',
'update: h1,opt2',
'destroy',
'create: h2,opt2'
]);
assert.htmlEqual(target.innerHTML, '<h2>tag is h2.</h2>');
component.tag = false;
assert.deepEqual(logs, [
'create: h1,opt1',
'update: h1,opt2',
'destroy',
'create: h2,opt2',
'destroy'
]);
assert.htmlEqual(target.innerHTML, '');
}
};

@ -0,0 +1,14 @@
<script>
export let logs = [];
export let tag = "h1";
export let opt = "opt1";
function foo(node, {tag, opt}) {
logs.push(`create: ${tag},${opt}`);
return {
update: ({tag, opt}) => logs.push(`update: ${tag},${opt}`),
destroy: () => logs.push('destroy'),
};
}
</script>
<svelte:element this={tag} use:foo={{tag, opt}}>tag is {tag}.</svelte:element>

@ -0,0 +1,105 @@
let originalDivGetBoundingClientRect;
let originalSpanGetBoundingClientRect;
let originalParagraphGetBoundingClientRect;
export default {
skip_if_ssr: true,
props: {
things: [
{ id: 1, name: 'a' },
{ id: 2, name: 'b' },
{ id: 3, name: 'c' },
{ id: 4, name: 'd' },
{ id: 5, name: 'e' }
],
tag: 'div'
},
html: `
<div>a</div>
<div>b</div>
<div>c</div>
<div>d</div>
<div>e</div>
`,
before_test() {
originalDivGetBoundingClientRect =
window.HTMLDivElement.prototype.getBoundingClientRect;
originalSpanGetBoundingClientRect =
window.HTMLSpanElement.prototype.getBoundingClientRect;
originalParagraphGetBoundingClientRect =
window.HTMLParagraphElement.prototype.getBoundingClientRect;
window.HTMLDivElement.prototype.getBoundingClientRect =
fakeGetBoundingClientRect;
window.HTMLSpanElement.prototype.getBoundingClientRect =
fakeGetBoundingClientRect;
window.HTMLParagraphElement.prototype.getBoundingClientRect =
fakeGetBoundingClientRect;
function fakeGetBoundingClientRect() {
const index = [...this.parentNode.children].indexOf(this);
const top = index * 30;
return {
left: 0,
right: 100,
top,
bottom: top + 20
};
}
},
after_test() {
window.HTMLDivElement.prototype.getBoundingClientRect =
originalDivGetBoundingClientRect;
window.HTMLSpanElement.prototype.getBoundingClientRect =
originalSpanGetBoundingClientRect;
window.HTMLParagraphElement.prototype.getBoundingClientRect =
originalParagraphGetBoundingClientRect;
},
async test({ assert, component, target, raf }) {
// switch tag and things at the same time
await component.update('p', [
{ id: 5, name: 'e' },
{ id: 2, name: 'b' },
{ id: 3, name: 'c' },
{ id: 4, name: 'd' },
{ id: 1, name: 'a' }
]);
const ps = document.querySelectorAll('p');
assert.equal(ps[0].dy, 120);
assert.equal(ps[4].dy, -120);
raf.tick(50);
assert.equal(ps[0].dy, 60);
assert.equal(ps[4].dy, -60);
raf.tick(100);
assert.equal(ps[0].dy, 0);
assert.equal(ps[4].dy, 0);
await component.update('span', [
{ id: 1, name: 'a' },
{ id: 2, name: 'b' },
{ id: 3, name: 'c' },
{ id: 4, name: 'd' },
{ id: 5, name: 'e' }
]);
const spans = document.querySelectorAll('span');
assert.equal(spans[0].dy, 120);
assert.equal(spans[4].dy, -120);
raf.tick(150);
assert.equal(spans[0].dy, 60);
assert.equal(spans[4].dy, -60);
raf.tick(200);
assert.equal(spans[0].dy, 0);
assert.equal(spans[4].dy, 0);
}
};

@ -0,0 +1,26 @@
<script>
export let things;
export let tag;
function flip(node, animation, params) {
const dx = animation.from.left - animation.to.left;
const dy = animation.from.top - animation.to.top;
return {
duration: 100,
tick: (t, u) => {
node.dx = u * dx;
node.dy = u * dy;
}
};
}
export function update(new_tag, new_things) {
things = new_things;
tag = new_tag;
}
</script>
{#each things as thing (thing.id)}
<svelte:element this={tag} animate:flip>{thing.name}</svelte:element>
{/each}

@ -0,0 +1,62 @@
export default {
props: {
things: [
{ id: 1, name: 'a' },
{ id: 2, name: 'b' },
{ id: 3, name: 'c' },
{ id: 4, name: 'd' },
{ id: 5, name: 'e' }
],
tag: 'div'
},
html: `
<div>a</div>
<div>b</div>
<div>c</div>
<div>d</div>
<div>e</div>
`,
test({ assert, component, target, raf }) {
component.tag = 'p';
assert.equal(target.querySelectorAll('p').length, 5);
component.tag = 'div';
let divs = target.querySelectorAll('div');
divs.forEach(div => {
div.getBoundingClientRect = function() {
const index = [...this.parentNode.children].indexOf(this);
const top = index * 30;
return {
left: 0,
right: 100,
top,
bottom: top + 20
};
};
});
component.things = [
{ id: 5, name: 'e' },
{ id: 2, name: 'b' },
{ id: 3, name: 'c' },
{ id: 4, name: 'd' },
{ id: 1, name: 'a' }
];
divs = target.querySelectorAll('div');
assert.ok(~divs[0].style.animation.indexOf('__svelte'));
assert.equal(divs[1].style.animation, '');
assert.equal(divs[2].style.animation, '');
assert.equal(divs[3].style.animation, '');
assert.ok(~divs[4].style.animation.indexOf('__svelte'));
raf.tick(100);
assert.deepEqual([
divs[0].style.animation,
divs[4].style.animation
], ['', '']);
}
};

@ -0,0 +1,18 @@
<script>
export let things;
export let tag;
function flip(node, animation, params) {
const dx = animation.from.left - animation.to.left;
const dy = animation.from.top - animation.to.top;
return {
duration: 100,
css: (t, u) => `transform: translate(${u + dx}px, ${u * dy}px)`
};
}
</script>
{#each things as thing (thing.id)}
<svelte:element this={tag} animate:flip>{thing.name}</svelte:element>
{/each}

@ -0,0 +1,17 @@
export default {
props: {
tag: 'div'
},
html: '<div style="color: red;">Foo</div>',
test({ assert, component, target }) {
component.tag = 'h1';
assert.htmlEqual(
target.innerHTML,
`
<h1 style="color: red;">Foo</h1>
`
);
}
};

@ -0,0 +1,5 @@
<script>
export let tag = 'div';
</script>
<svelte:element this={tag} style="color: red;">Foo</svelte:element>

@ -0,0 +1,3 @@
export default {
error: "'value' is not a valid binding on <svelte:element> elements"
};

@ -0,0 +1,6 @@
<script>
const tag = "div";
let value;
</script>
<svelte:element this={tag} bind:value></svelte:element>

@ -0,0 +1,8 @@
export default {
html: '<div></div>',
test({ assert, component, target }) {
const div = target.querySelector('div');
assert.equal(div, component.foo);
}
};

@ -0,0 +1,6 @@
<script>
const tag = "div";
export let foo;
</script>
<svelte:element this={tag} bind:this={foo}></svelte:element>

@ -0,0 +1,17 @@
export default {
props: {
tag: 'div'
},
html: '<div>Foo</div>',
test({ assert, component, target }) {
component.tag = 'h1';
assert.htmlEqual(
target.innerHTML,
`
<h1>Foo</h1>
`
);
}
};

@ -0,0 +1,5 @@
<script>
export let tag = 'div';
</script>
<svelte:element this={tag}>Foo</svelte:element>

@ -0,0 +1,5 @@
<script>
let tag = '';
</script>
<svelte:element this={tag}>Foo</svelte:element>

@ -0,0 +1,21 @@
let clicked = false;
function handler() {
clicked = true;
}
export default {
props: {
handler
},
html: '<button>Foo</button>',
test({ assert, target }) {
assert.equal(clicked, false);
const button = target.querySelector('button');
const click = new window.MouseEvent('click');
button.dispatchEvent(click);
assert.equal(clicked, true);
}
};

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save