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
## 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
* 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",
"version": "3.46.6",
"version": "3.47.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "svelte",
"version": "3.46.6",
"version": "3.47.0",
"license": "MIT",
"devDependencies": {
"@ampproject/remapping": "^0.3.0",

@ -1,6 +1,6 @@
{
"name": "svelte",
"version": "3.46.6",
"version": "3.47.0",
"description": "Cybernetically enhanced web apps",
"module": "index.mjs",
"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
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>

@ -1627,6 +1627,28 @@ If `this` is falsy, no component is rendered.
<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>`
@ -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.
`<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
<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.
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
<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.

@ -2,11 +2,7 @@
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:
* [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)
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.
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`.
| `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"`.
| `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)}`
| `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.

@ -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:
- [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)

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

@ -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.
> 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',
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: {
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]`)'

@ -18,6 +18,9 @@ import Let from './Let';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
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_errors from '../compiler_errors';
@ -190,11 +193,26 @@ export default class Element extends Node {
children: INode[];
namespace: string;
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) {
super(component, parent, scope, info);
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);
if (this.namespace !== namespaces.foreign) {

@ -254,11 +254,17 @@ export default class Expression {
const declaration = b`const ${id} = ${node}`;
if (owner.type === 'ConstTag') {
let child_scope = scope;
walk(node, {
enter(node: Node) {
if (node.type === 'Identifier') {
enter(node: Node, parent: any) {
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));
}
},
leave(node: Node) {
if (map.has(node)) child_scope = child_scope.parent;
}
});
} else if (dependencies.size === 0 && contextual_dependencies.size === 0) {

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

@ -26,6 +26,7 @@ import Action from '../../../nodes/Action';
import MustacheTagWrapper from '../MustacheTag';
import RawMustacheTagWrapper from '../RawMustacheTag';
import is_dynamic from '../shared/is_dynamic';
import create_debugging_comment from '../shared/create_debugging_comment';
import { push_array } from '../../../../utils/push_array';
interface BindingGroup {
@ -134,6 +135,8 @@ const events = [
}
];
const CHILD_DYNAMIC_ELEMENT_BLOCK = 'child_dynamic_element';
export default class ElementWrapper extends Wrapper {
node: Element;
fragment: FragmentWrapper;
@ -147,6 +150,9 @@ export default class ElementWrapper extends Wrapper {
var: any;
void: boolean;
child_dynamic_element_block?: Block = null;
child_dynamic_element?: ElementWrapper = null;
constructor(
renderer: Renderer,
block: Block,
@ -156,6 +162,24 @@ export default class ElementWrapper extends Wrapper {
next_sibling: Wrapper
) {
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 = {
type: 'Identifier',
name: node.name.replace(/[^a-zA-Z0-9_$]/g, '_')
@ -199,6 +223,8 @@ export default class ElementWrapper extends Wrapper {
block.add_animation();
}
block.add_dependencies(node.tag_expr.dependencies);
// add directive and handler dependencies
[node.animation, node.outro, ...node.actions, ...node.classes, ...node.styles].forEach(directive => {
if (directive && directive.expression) {
@ -221,6 +247,7 @@ export default class ElementWrapper extends Wrapper {
node.handlers.length > 0 ||
node.styles.length > 0 ||
this.node.name === 'option' ||
node.tag_expr.dynamic_dependencies().length ||
renderer.options.dev
) {
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) {
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;
if (this.node.name === 'noscript') return;
@ -249,7 +380,7 @@ export default class ElementWrapper extends Wrapper {
if (renderer.options.hydratable) {
if (parent_nodes) {
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) {
@ -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});`
);
}
block.renderer.dirty(this.node.tag_expr.dynamic_dependencies());
}
can_use_textcontent() {
@ -364,7 +497,7 @@ export default class ElementWrapper extends Wrapper {
}
get_render_statement(block: Block) {
const { name, namespace } = this.node;
const { name, namespace, tag_expr } = this.node;
if (namespace === namespaces.svg) {
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("${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
.filter((attr) => !(attr instanceof SpreadAttributeWrapper) && !attr.property_name)
.map((attr) => p`${(attr as StyleAttributeWrapper | AttributeWrapper).name}: true`);
const name = this.node.namespace
? this.node.name
: this.node.name.toUpperCase();
let reference;
if (this.node.tag_expr.node.type === 'Literal') {
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) {
return x`@claim_svg_element(${nodes}, "${name}", { ${attributes} })`;
return x`@claim_svg_element(${nodes}, ${reference}, { ${attributes} })`;
} 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();
`);
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`
@fix_position(${this.var});
${stop_animation}();

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

@ -8,6 +8,7 @@ import Expression from '../../nodes/shared/Expression';
import remove_whitespace_children from './utils/remove_whitespace_children';
import fix_attribute_casing from '../../render_dom/wrappers/Element/fix_attribute_casing';
import { namespaces } from '../../../utils/namespaces';
import { Expression as ESExpression } from 'estree';
export default function (node: Element, renderer: Renderer, options: RenderOptions) {
@ -22,7 +23,8 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
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 { expression, name } = class_directive;
@ -167,14 +169,25 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
renderer.add_expression(node_contents);
}
if (!is_void(node.name)) {
renderer.add_string(`</${node.name}>`);
}
add_close_tag();
} else {
renderer.render(children, options);
add_close_tag();
}
function add_close_tag() {
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`,
message: `<${name}> cannot have children`
}),
invalid_element_definition: {
code: 'invalid-element-definition',
message: 'Invalid element definition'
},
invalid_element_placement: (slug: string, name: string) => ({
code: `invalid-${slug}-placement`,
message: `<${name}> tags cannot be inside elements or blocks`
@ -161,6 +165,10 @@ export default {
code: 'missing-attribute-value',
message: 'Expected value for the attribute'
},
missing_element_definition: {
code: 'missing-element-definition',
message: '<svelte:element> must have a \'this\' attribute'
},
unclosed_script: {
code: 'unclosed-script',
message: '<script> must have a closing tag'

@ -19,7 +19,7 @@ const meta_tags = new Map([
['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([
[
@ -41,6 +41,7 @@ const specials = new Map([
const SELF = /^svelte:self(?=[\s/>])/;
const COMPONENT = /^svelte:component(?=[\s/>])/;
const SLOT = /^svelte:fragment(?=[\s/>])/;
const ELEMENT = /^svelte:element(?=[\s/>])/;
function parent_is_head(stack) {
let i = stack.length;
@ -169,7 +170,7 @@ export default function tag(parser: Parser) {
if (name === 'svelte:component') {
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);
}
@ -181,6 +182,19 @@ export default function tag(parser: Parser) {
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>
if (specials.has(name) && parser.stack.length === 1) {
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(ELEMENT)) return 'svelte:element';
if (parser.read(SLOT)) return 'svelte:fragment';

@ -56,6 +56,7 @@ export const globals = new Set([
'setTimeout',
'String',
'SVGElement',
'Symbol',
'SyntaxError',
'TypeError',
'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>;
export interface SvelteComponentDev {
$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);
return context;
}
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>

@ -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",
"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,
"start": {
"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);
}
};

@ -0,0 +1,6 @@
<script>
const tag = "button";
export let handler;
</script>
<svelte:element this={tag} on:click={handler}>Foo</svelte:element>

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

Loading…
Cancel
Save