Merge branch 'master' into ts-post

pull/5101/head
Rich Harris 5 years ago
commit 0d0fabb284

@ -3,6 +3,7 @@ on: [push, pull_request]
jobs: jobs:
Tests: Tests:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 10
strategy: strategy:
matrix: matrix:
node-version: [8, 10, 12, 14] node-version: [8, 10, 12, 14]
@ -18,12 +19,14 @@ jobs:
CI: true CI: true
Lint: Lint:
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 2
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- uses: actions/setup-node@v1 - uses: actions/setup-node@v1
- run: 'npm i && npm run lint' - run: 'npm i && npm run lint'
Unit: Unit:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 5
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macOS-latest] os: [ubuntu-latest, windows-latest, macOS-latest]

@ -2,16 +2,26 @@
## Unreleased ## Unreleased
* Fix reactivity when passing `$$props` to a `<slot>` ([#3364](https://github.com/sveltejs/svelte/issues/3364))
* Fix unneeded invalidation of `$$props` and `$$restProps` ([#4993](https://github.com/sveltejs/svelte/issues/4993), [#5118](https://github.com/sveltejs/svelte/issues/5118))
## 3.24.0
* Support nullish coalescing (`??`) and optional chaining (`?.`) operators ([#1972](https://github.com/sveltejs/svelte/issues/1972)) * Support nullish coalescing (`??`) and optional chaining (`?.`) operators ([#1972](https://github.com/sveltejs/svelte/issues/1972))
* Support `import.meta` ([#4379](https://github.com/sveltejs/svelte/issues/4379)) * Support `import.meta` ([#4379](https://github.com/sveltejs/svelte/issues/4379))
* Fix placement of `{@html}` when used at the root of a slot or the root of a component ([#5012](https://github.com/sveltejs/svelte/issues/5012)) * Fix only setting `<input>` values when they're changed when there are spread attributes ([#4418](https://github.com/sveltejs/svelte/issues/4418))
* Fix placement of `{@html}` when used at the root of a slot, at the root of a component, or in `<svelte:head>` ([#5012](https://github.com/sveltejs/svelte/issues/5012), [#5071](https://github.com/sveltejs/svelte/pull/5071))
* Fix certain handling of two-way bound `contenteditable` elements ([#5018](https://github.com/sveltejs/svelte/issues/5018))
* Fix handling of `import`ed value that is used as a store and is also mutated ([#5019](https://github.com/sveltejs/svelte/issues/5019)) * Fix handling of `import`ed value that is used as a store and is also mutated ([#5019](https://github.com/sveltejs/svelte/issues/5019))
* Do not display `a11y-missing-content` warning on elements with `contenteditable` bindings ([#5020](https://github.com/sveltejs/svelte/issues/5020)) * Do not display `a11y-missing-content` warning on elements with `contenteditable` bindings ([#5020](https://github.com/sveltejs/svelte/issues/5020))
* Fix handling of `this` in inline function expressions in the template ([#5033](https://github.com/sveltejs/svelte/issues/5033)) * Fix handling of `this` in inline function expressions in the template ([#5033](https://github.com/sveltejs/svelte/issues/5033))
* Fix collapsing HTML with static content ([#5040](https://github.com/sveltejs/svelte/issues/5040)) * Fix collapsing HTML with static content ([#5040](https://github.com/sveltejs/svelte/issues/5040))
* Prevent use of `$store` at compile time when top-level `store` has been shadowed ([#5048](https://github.com/sveltejs/svelte/issues/5048))
* Update `<select>` with one-way `value` binding when the available `<option>`s change ([#5051](https://github.com/sveltejs/svelte/issues/5051)) * Update `<select>` with one-way `value` binding when the available `<option>`s change ([#5051](https://github.com/sveltejs/svelte/issues/5051))
* Fix published `tweened` types so the `.set()` and `.update()` options are optional ([#5062](https://github.com/sveltejs/svelte/issues/5062)) * Fix published `tweened` types so the `.set()` and `.update()` options are optional ([#5062](https://github.com/sveltejs/svelte/issues/5062))
* Fix contextual `bind:this` inside `{#each}` block ([#5067](https://github.com/sveltejs/svelte/issues/5067)) * Fix contextual `bind:this` inside `{#each}` block ([#5067](https://github.com/sveltejs/svelte/issues/5067))
* Preprocess self-closing `<script>` and `<style>` tags ([#5080](https://github.com/sveltejs/svelte/issues/5080))
* Fix types for animation- and transition-related param objects so each param is optional ([#5083](https://github.com/sveltejs/svelte/pull/5083))
## 3.23.2 ## 3.23.2

8
package-lock.json generated

@ -1,6 +1,6 @@
{ {
"name": "svelte", "name": "svelte",
"version": "3.23.2", "version": "3.24.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -2527,9 +2527,9 @@
} }
}, },
"lodash": { "lodash": {
"version": "4.17.15", "version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
"dev": true "dev": true
}, },
"lodash.sortby": { "lodash.sortby": {

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

@ -30,7 +30,7 @@ Given that, what if the framework *didn't actually run in the browser*? What if,
Svelte is a new framework that does exactly that. You write your components using HTML, CSS and JavaScript (plus a few extra bits you can [learn in under 5 minutes](https://v2.svelte.dev/guide)), and during your build process Svelte compiles them into tiny standalone JavaScript modules. By statically analysing the component template, we can make sure that the browser does as little work as possible. Svelte is a new framework that does exactly that. You write your components using HTML, CSS and JavaScript (plus a few extra bits you can [learn in under 5 minutes](https://v2.svelte.dev/guide)), and during your build process Svelte compiles them into tiny standalone JavaScript modules. By statically analysing the component template, we can make sure that the browser does as little work as possible.
The [Svelte implementation of TodoMVC](http://svelte-todomvc.surge.sh/) weighs 3.6kb zipped. For comparison, React plus ReactDOM *without any app code* weighs about 45kb zipped. It takes about 10x as long for the browser just to evaluate React as it does for Svelte to be up and running with an interactive TodoMVC. The [Svelte implementation of TodoMVC](https://svelte-todomvc.surge.sh/) weighs 3.6kb zipped. For comparison, React plus ReactDOM *without any app code* weighs about 45kb zipped. It takes about 10x as long for the browser just to evaluate React as it does for Svelte to be up and running with an interactive TodoMVC.
And once your app *is* up and running, according to [js-framework-benchmark](https://github.com/krausest/js-framework-benchmark) **Svelte is fast as heck**. It's faster than React. It's faster than Vue. It's faster than Angular, or Ember, or Ractive, or Preact, or Riot, or Mithril. It's competitive with Inferno, which is probably the fastest UI framework in the world, for now, because [Dominic Gannaway](https://twitter.com/trueadm) is a wizard. (Svelte is slower at removing elements. We're [working on it](https://github.com/sveltejs/svelte/issues/26).) And once your app *is* up and running, according to [js-framework-benchmark](https://github.com/krausest/js-framework-benchmark) **Svelte is fast as heck**. It's faster than React. It's faster than Vue. It's faster than Angular, or Ember, or Ractive, or Preact, or Riot, or Mithril. It's competitive with Inferno, which is probably the fastest UI framework in the world, for now, because [Dominic Gannaway](https://twitter.com/trueadm) is a wizard. (Svelte is slower at removing elements. We're [working on it](https://github.com/sveltejs/svelte/issues/26).)

@ -57,7 +57,7 @@ What about a more 'real world' example? Conveniently, the [RealWorld](https://gi
<aside><p>Code-splitting isn't free — if the reference implementation used code-splitting, it would be larger still</p></aside> <aside><p>Code-splitting isn't free — if the reference implementation used code-splitting, it would be larger still</p></aside>
The entire app costs 132.7kb (39.9kb zipped), which is significantly smaller than the reference React/Redux implementation at 327kb (85.7kb), but even if was as large it would *feel* faster because of code-splitting. And that's a crucial point. We're told we need to code-split our apps, but if your app uses a traditional framework like React or Vue then there's a hard lower bound on the size of your initial code-split chunk — the framework itself, which is likely to be a significant portion of your total app size. With the Svelte approach, that's no longer the case. The entire app costs 132.7kb (39.9kb zipped), which is significantly smaller than the reference React/Redux implementation at 327kb (85.7kb), but even if it was as large it would *feel* faster because of code-splitting. And that's a crucial point. We're told we need to code-split our apps, but if your app uses a traditional framework like React or Vue then there's a hard lower bound on the size of your initial code-split chunk — the framework itself, which is likely to be a significant portion of your total app size. With the Svelte approach, that's no longer the case.
But size is only part of the story. Svelte apps are also extremely performant and memory-efficient, and the framework includes powerful features that you would sacrifice if you chose a 'minimal' or 'simple' UI library. But size is only part of the story. Svelte apps are also extremely performant and memory-efficient, and the framework includes powerful features that you would sacrifice if you chose a 'minimal' or 'simple' UI library.

@ -74,8 +74,6 @@ This creates a new directory, `my-svelte-project`, adds files from the [sveltejs
In the `package.json` file, there is a section called `"scripts"`. These scripts define shortcuts for working with your application — `dev`, `build` and `start`. To launch your app in development mode, type the following: In the `package.json` file, there is a section called `"scripts"`. These scripts define shortcuts for working with your application — `dev`, `build` and `start`. To launch your app in development mode, type the following:
> TODO update the template, it needs... some work
```bash ```bash
npm run dev npm run dev
``` ```

@ -16,16 +16,10 @@
$: remaining = todos.filter(t => !t.done).length; $: remaining = todos.filter(t => !t.done).length;
</script> </script>
<style>
.done {
opacity: 0.4;
}
</style>
<h1>Todos</h1> <h1>Todos</h1>
{#each todos as todo} {#each todos as todo}
<div class:done={todo.done}> <div>
<input <input
type=checkbox type=checkbox
bind:checked={todo.done} bind:checked={todo.done}
@ -34,6 +28,7 @@
<input <input
placeholder="What needs to be done?" placeholder="What needs to be done?"
bind:value={todo.text} bind:value={todo.text}
disabled={todo.done}
> >
</div> </div>
{/each} {/each}

@ -1,5 +1,5 @@
--- ---
question: How can I get VSCode to syntax-highlight my .svelte files? question: How can I get VS Code to syntax-highlight my .svelte files?
--- ---
There is an [official VSCode extension for Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode), however it is still in the **beta** testing stage, and not all issues have been ironed out. There is an [official VS Code extension for Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode).

@ -0,0 +1,32 @@
---
question: How do I document my components?
---
In editors which use the Svelte Language Server you can document Components, functions and exports using specially formatted comments.
````svelte
<script>
/** What should we call the user? */
export let name = 'world';
</script>
<!--
@component
Here's some documentation for this component.
It will show up on hover.
- You can use markdown here.
- You can also use code blocks here.
- Usage:
```tsx
<main name="Arethra">
```
-->
<main>
<h1>
Hello, {name}
</h1>
</main>
````
Note: The `@component` is necessary in the HTML comment which describes your component.

@ -1,10 +1,11 @@
--- ---
question: What about Typescript support? question: What about TypeScript support?
--- ---
You need to install a preprocessor such as [svelte-preprocess](https://github.com/sveltejs/svelte-preprocess). Work is ongoing to improve [IDE support](https://github.com/sveltejs/language-tools/issues/83). You can also run type checking from the command line with [svelte-check](https://www.npmjs.com/package/svelte-check). You need to install a preprocessor such as [svelte-preprocess](https://github.com/sveltejs/svelte-preprocess). You can run type checking from the command line with [svelte-check](https://www.npmjs.com/package/svelte-check).
To declare the type of a reactive variable in a Svelte template, you should use the following syntax:
To declare the type of a reactive variable in a Svelte template, you can use the following syntax:
``` ```
let x: number; let x: number;
$: x = count + 1; $: x = count + 1;

@ -1281,9 +1281,9 @@
} }
}, },
"@sveltejs/site-kit": { "@sveltejs/site-kit": {
"version": "1.1.5", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@sveltejs/site-kit/-/site-kit-1.1.5.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/site-kit/-/site-kit-1.2.0.tgz",
"integrity": "sha512-Rs2quQ/H00DAN/ZTFa+unLefL335UW3Yo4I2rTocW5JwW73Kvi5++d7BcY8LsjhMCbG1PkwQmJE2RVrIIxQcOw==", "integrity": "sha512-C7puq+1so3fKPPZAnQJQlKfyCG6FsnSSFSS2LRIhWD8VK2FL5j8Eq7DIKSxUvWbGw1AsxnzO3dIHJWPB7fwjKg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@sindresorhus/slugify": "^0.9.1", "@sindresorhus/slugify": "^0.9.1",
@ -1291,9 +1291,9 @@
} }
}, },
"@sveltejs/svelte-repl": { "@sveltejs/svelte-repl": {
"version": "0.1.20", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/@sveltejs/svelte-repl/-/svelte-repl-0.1.20.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/svelte-repl/-/svelte-repl-0.2.0.tgz",
"integrity": "sha512-PPy1Sxn+yLSo+H/KPvl8jRJizv4VgkDLL2u3SLfSgOgQgzUVBEjiOW9HUlgOwW61uCq2lSsS7A8NdILxEkrFnA==", "integrity": "sha512-2vLQnOVrsmn2d2K4a6urGm8OulGGSPhZCGNySSb1H8nOPsgKrdcTt5qoaxNYXgcyVp55Yow2SvXYXsyJKd4KEQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"codemirror": "^5.49.2", "codemirror": "^5.49.2",
@ -2404,9 +2404,9 @@
} }
}, },
"lodash": { "lodash": {
"version": "4.17.15", "version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==", "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==",
"dev": true "dev": true
}, },
"lodash.deburr": { "lodash.deburr": {

@ -36,8 +36,8 @@
"@babel/preset-env": "^7.6.0", "@babel/preset-env": "^7.6.0",
"@babel/runtime": "^7.6.0", "@babel/runtime": "^7.6.0",
"@sindresorhus/slugify": "^0.9.1", "@sindresorhus/slugify": "^0.9.1",
"@sveltejs/site-kit": "^1.1.5", "@sveltejs/site-kit": "^1.2.0",
"@sveltejs/svelte-repl": "^0.1.20", "@sveltejs/svelte-repl": "^0.2.0",
"degit": "^2.1.4", "degit": "^2.1.4",
"dotenv": "^8.1.0", "dotenv": "^8.1.0",
"esm": "^3.2.25", "esm": "^3.2.25",

@ -85,7 +85,7 @@ export default function() {
renderer.heading = (text, level, rawtext) => { renderer.heading = (text, level, rawtext) => {
let slug; let slug;
const match = /<a href="([^"]+)">(.+)<\/a>/.exec(text); const match = /<a href="([^"]+)"[^>]*>(.+)<\/a>/.exec(text);
if (match) { if (match) {
slug = match[1]; slug = match[1];
text = match[2]; text = match[2];

@ -227,6 +227,7 @@ export default app;` });
padding: .6rem var(--side-nav); padding: .6rem var(--side-nav);
background-color: var(--second); background-color: var(--second);
color: white; color: white;
white-space: nowrap;
} }
.icon { .icon {

@ -836,7 +836,7 @@ export default class Component {
}); });
} }
warn_on_undefined_store_value_references(node, parent, scope) { warn_on_undefined_store_value_references(node, parent, scope: Scope) {
if ( if (
node.type === 'LabeledStatement' && node.type === 'LabeledStatement' &&
node.label.name === '$' && node.label.name === '$' &&
@ -852,8 +852,17 @@ export default class Component {
const object = get_object(node); const object = get_object(node);
const { name } = object; const { name } = object;
if (name[0] === '$' && !scope.has(name)) { if (name[0] === '$') {
this.warn_if_undefined(name, object, null); if (!scope.has(name)) {
this.warn_if_undefined(name, object, null);
}
if (name[1] !== '$' && scope.has(name.slice(1)) && scope.find_owner(name.slice(1)) !== this.instance_scope) {
this.error(node, {
code: `contextual-store`,
message: `Stores must be declared at the top level of the component (this may change in a future version of Svelte)`
});
}
} }
} }
} }

@ -78,11 +78,14 @@ export default class Expression {
if (scope.has(name)) return; if (scope.has(name)) return;
if (name[0] === '$' && template_scope.names.has(name.slice(1))) { if (name[0] === '$') {
component.error(node, { const store_name = name.slice(1);
code: `contextual-store`, if (template_scope.names.has(store_name) || scope.has(store_name)) {
message: `Stores must be declared at the top level of the component (this may change in a future version of Svelte)` component.error(node, {
}); code: `contextual-store`,
message: `Stores must be declared at the top level of the component (this may change in a future version of Svelte)`
});
}
} }
if (template_scope.is_let(name)) { if (template_scope.is_let(name)) {

@ -86,6 +86,7 @@ export default function dom(
const set = (uses_props || uses_rest || writable_props.length > 0 || component.slots.size > 0) const set = (uses_props || uses_rest || writable_props.length > 0 || component.slots.size > 0)
? x` ? x`
${$$props} => { ${$$props} => {
${(uses_props || uses_rest) && b`if (@is_empty(${$$props})) return;`}
${uses_props && renderer.invalidate('$$props', x`$$props = @assign(@assign({}, $$props), @exclude_internal_props($$new_props))`)} ${uses_props && renderer.invalidate('$$props', x`$$props = @assign(@assign({}, $$props), @exclude_internal_props($$new_props))`)}
${uses_rest && !uses_props && x`$$props = @assign(@assign({}, $$props), @exclude_internal_props($$new_props))`} ${uses_rest && !uses_props && x`$$props = @assign(@assign({}, $$props), @exclude_internal_props($$new_props))`}
${uses_rest && renderer.invalidate('$$restProps', x`$$restProps = ${compute_rest}`)} ${uses_rest && renderer.invalidate('$$restProps', x`$$restProps = ${compute_rest}`)}

@ -7,8 +7,9 @@ import { b, x } from 'code-red';
import Expression from '../../../nodes/shared/Expression'; import Expression from '../../../nodes/shared/Expression';
import Text from '../../../nodes/Text'; import Text from '../../../nodes/Text';
import handle_select_value_binding from './handle_select_value_binding'; import handle_select_value_binding from './handle_select_value_binding';
import { Identifier, Node } from 'estree';
export default class AttributeWrapper { export class BaseAttributeWrapper {
node: Attribute; node: Attribute;
parent: ElementWrapper; parent: ElementWrapper;
@ -21,7 +22,29 @@ export default class AttributeWrapper {
parent.not_static_content(); parent.not_static_content();
block.add_dependencies(node.dependencies); block.add_dependencies(node.dependencies);
}
}
render(_block: Block) {}
}
export default class AttributeWrapper extends BaseAttributeWrapper {
node: Attribute;
parent: ElementWrapper;
metadata: any;
name: string;
property_name: string;
is_indirectly_bound_value: boolean;
is_src: boolean;
is_select_value_attribute: boolean;
is_input_value: boolean;
should_cache: boolean;
last: Identifier;
constructor(parent: ElementWrapper, block: Block, node: Attribute) {
super(parent, block, node);
if (node.dependencies.size > 0) {
// special case — <option value={foo}> — see below // special case — <option value={foo}> — see below
if (this.parent.node.name === 'option' && node.name === 'value') { if (this.parent.node.name === 'option' && node.name === 'value') {
let select: ElementWrapper = this.parent; let select: ElementWrapper = this.parent;
@ -42,31 +65,22 @@ export default class AttributeWrapper {
handle_select_value_binding(this, node.dependencies); handle_select_value_binding(this, node.dependencies);
} }
} }
}
is_indirectly_bound_value() { this.name = fix_attribute_casing(this.node.name);
const element = this.parent; this.metadata = this.get_metadata();
const name = fix_attribute_casing(this.node.name); this.is_indirectly_bound_value = is_indirectly_bound_value(this);
return name === 'value' && this.property_name = this.is_indirectly_bound_value
(element.node.name === 'option' || // TODO check it's actually bound ? '__value'
(element.node.name === 'input' && : this.metadata && this.metadata.property_name;
element.node.bindings.some( this.is_src = this.name === 'src'; // TODO retire this exception in favour of https://github.com/sveltejs/svelte/issues/3750
(binding) => this.is_select_value_attribute = this.name === 'value' && this.parent.node.name === 'select';
/checked|group/.test(binding.name) this.is_input_value = this.name === 'value' && this.parent.node.name === 'input';
))); this.should_cache = should_cache(this);
} }
render(block: Block) { render(block: Block) {
const element = this.parent; const element = this.parent;
const name = fix_attribute_casing(this.node.name); const { name, property_name, should_cache, is_indirectly_bound_value } = this;
const metadata = this.get_metadata();
const is_indirectly_bound_value = this.is_indirectly_bound_value();
const property_name = is_indirectly_bound_value
? '__value'
: metadata && metadata.property_name;
// xlink is a special case... we could maybe extend this to generic // xlink is a special case... we could maybe extend this to generic
// namespaced attributes but I'm not sure that's applicable in // namespaced attributes but I'm not sure that's applicable in
@ -82,29 +96,15 @@ export default class AttributeWrapper {
const dependencies = this.get_dependencies(); const dependencies = this.get_dependencies();
const value = this.get_value(block); const value = this.get_value(block);
const is_src = this.node.name === 'src'; // TODO retire this exception in favour of https://github.com/sveltejs/svelte/issues/3750
const is_select_value_attribute =
name === 'value' && element.node.name === 'select';
const is_input_value = name === 'value' && element.node.name === 'input';
const should_cache = is_src || this.node.should_cache();
const last = should_cache && block.get_unique_name(
`${element.var.name}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value`
);
if (should_cache) block.add_variable(last);
let updater; let updater;
const init = should_cache ? x`${last} = ${value}` : value; const init = this.get_init(block, value);
if (is_legacy_input_type) { if (is_legacy_input_type) {
block.chunks.hydrate.push( block.chunks.hydrate.push(
b`@set_input_type(${element.var}, ${init});` b`@set_input_type(${element.var}, ${init});`
); );
updater = b`@set_input_type(${element.var}, ${should_cache ? last : value});`; updater = b`@set_input_type(${element.var}, ${should_cache ? this.last : value});`;
} else if (is_select_value_attribute) { } else if (this.is_select_value_attribute) {
// annoying special case // annoying special case
const is_multiple_select = element.node.get_static_attribute_value('multiple'); const is_multiple_select = element.node.get_static_attribute_value('multiple');
@ -117,45 +117,37 @@ export default class AttributeWrapper {
block.chunks.mount.push(b` block.chunks.mount.push(b`
${updater} ${updater}
`); `);
} else if (is_src) { } else if (this.is_src) {
block.chunks.hydrate.push( block.chunks.hydrate.push(
b`if (${element.var}.src !== ${init}) ${method}(${element.var}, "${name}", ${last});` b`if (${element.var}.src !== ${init}) ${method}(${element.var}, "${name}", ${this.last});`
); );
updater = b`${method}(${element.var}, "${name}", ${should_cache ? last : value});`; updater = b`${method}(${element.var}, "${name}", ${should_cache ? this.last : value});`;
} else if (property_name) { } else if (property_name) {
block.chunks.hydrate.push( block.chunks.hydrate.push(
b`${element.var}.${property_name} = ${init};` b`${element.var}.${property_name} = ${init};`
); );
updater = block.renderer.options.dev updater = block.renderer.options.dev
? b`@prop_dev(${element.var}, "${property_name}", ${should_cache ? last : value});` ? b`@prop_dev(${element.var}, "${property_name}", ${should_cache ? this.last : value});`
: b`${element.var}.${property_name} = ${should_cache ? last : value};`; : b`${element.var}.${property_name} = ${should_cache ? this.last : value};`;
} else { } else {
block.chunks.hydrate.push( block.chunks.hydrate.push(
b`${method}(${element.var}, "${name}", ${init});` b`${method}(${element.var}, "${name}", ${init});`
); );
updater = b`${method}(${element.var}, "${name}", ${should_cache ? last : value});`; updater = b`${method}(${element.var}, "${name}", ${should_cache ? this.last : value});`;
} }
if (dependencies.length > 0) { if (is_indirectly_bound_value) {
let condition = block.renderer.dirty(dependencies); const update_value = b`${element.var}.value = ${element.var}.__value;`;
block.chunks.hydrate.push(update_value);
if (should_cache) {
condition = is_src
? x`${condition} && (${element.var}.src !== (${last} = ${value}))`
: x`${condition} && (${last} !== (${last} = ${value}))`;
}
if (is_input_value) {
const type = element.node.get_static_attribute_value('type');
if (type === null || type === "" || type === "text" || type === "email" || type === "password") { updater = b`
condition = x`${condition} && ${element.var}.${property_name} !== ${should_cache ? last : value}`; ${updater}
} ${update_value};
} `;
}
if (block.has_outros) { if (dependencies.length > 0) {
condition = x`!#current || ${condition}`; const condition = this.get_dom_update_conditions(block, block.renderer.dirty(dependencies));
}
block.chunks.update.push(b` block.chunks.update.push(b`
if (${condition}) { if (${condition}) {
@ -167,13 +159,44 @@ export default class AttributeWrapper {
if (this.node.is_true && name === 'autofocus') { if (this.node.is_true && name === 'autofocus') {
block.autofocus = element.var; block.autofocus = element.var;
} }
}
if (is_indirectly_bound_value) { get_init(block: Block, value) {
const update_value = b`${element.var}.value = ${element.var}.__value;`; this.last = this.should_cache && block.get_unique_name(
`${this.parent.var.name}_${this.name.replace(/[^a-zA-Z_$]/g, '_')}_value`
);
block.chunks.hydrate.push(update_value); if (this.should_cache) block.add_variable(this.last);
if (dependencies.length > 0) block.chunks.update.push(update_value);
return this.should_cache ? x`${this.last} = ${value}` : value;
}
get_dom_update_conditions(block: Block, dependency_condition: Node) {
const { property_name, should_cache, last } = this;
const element = this.parent;
const value = this.get_value(block);
let condition = dependency_condition;
if (should_cache) {
condition = this.is_src
? x`${condition} && (${element.var}.src !== (${last} = ${value}))`
: x`${condition} && (${last} !== (${last} = ${value}))`;
}
if (this.is_input_value) {
const type = element.node.get_static_attribute_value('type');
if (type === null || type === "" || type === "text" || type === "email" || type === "password") {
condition = x`${condition} && ${element.var}.${property_name} !== ${should_cache ? last : value}`;
}
}
if (block.has_outros) {
condition = x`!#current || ${condition}`;
} }
return condition;
} }
get_dependencies() { get_dependencies() {
@ -194,15 +217,14 @@ export default class AttributeWrapper {
get_metadata() { get_metadata() {
if (this.parent.node.namespace) return null; if (this.parent.node.namespace) return null;
const metadata = attribute_lookup[fix_attribute_casing(this.node.name)]; const metadata = attribute_lookup[this.name];
if (metadata && metadata.applies_to && !metadata.applies_to.includes(this.parent.node.name)) return null; if (metadata && metadata.applies_to && !metadata.applies_to.includes(this.parent.node.name)) return null;
return metadata; return metadata;
} }
get_value(block) { get_value(block) {
if (this.node.is_true) { if (this.node.is_true) {
const metadata = this.get_metadata(); if (this.metadata && boolean_attribute.has(this.metadata.property_name.toLowerCase())) {
if (metadata && boolean_attribute.has(metadata.property_name.toLowerCase())) {
return x`true`; return x`true`;
} }
return x`""`; return x`""`;
@ -350,4 +372,19 @@ const boolean_attribute = new Set([
'required', 'required',
'reversed', 'reversed',
'selected' 'selected'
]); ]);
function should_cache(attribute: AttributeWrapper) {
return attribute.is_src || attribute.node.should_cache();
}
function is_indirectly_bound_value(attribute: AttributeWrapper) {
const element = attribute.parent;
return attribute.name === 'value' &&
(element.node.name === 'option' || // TODO check it's actually bound
(element.node.name === 'input' &&
element.node.bindings.some(
(binding) =>
/checked|group/.test(binding.name)
)));
}

@ -0,0 +1,3 @@
import { BaseAttributeWrapper } from "./Attribute";
export default class SpreadAttributeWrapper extends BaseAttributeWrapper {}

@ -11,6 +11,7 @@ import { b, x, p } from 'code-red';
import { namespaces } from '../../../../utils/namespaces'; import { namespaces } from '../../../../utils/namespaces';
import AttributeWrapper from './Attribute'; import AttributeWrapper from './Attribute';
import StyleAttributeWrapper from './StyleAttribute'; import StyleAttributeWrapper from './StyleAttribute';
import SpreadAttributeWrapper from './SpreadAttribute';
import { dimensions } from '../../../../utils/patterns'; import { dimensions } from '../../../../utils/patterns';
import Binding from './Binding'; import Binding from './Binding';
import InlineComponentWrapper from '../InlineComponent'; import InlineComponentWrapper from '../InlineComponent';
@ -137,7 +138,7 @@ const events = [
export default class ElementWrapper extends Wrapper { export default class ElementWrapper extends Wrapper {
node: Element; node: Element;
fragment: FragmentWrapper; fragment: FragmentWrapper;
attributes: AttributeWrapper[]; attributes: Array<AttributeWrapper | StyleAttributeWrapper | SpreadAttributeWrapper>;
bindings: Binding[]; bindings: Binding[];
event_handlers: EventHandler[]; event_handlers: EventHandler[];
class_dependencies: string[]; class_dependencies: string[];
@ -221,6 +222,9 @@ export default class ElementWrapper extends Wrapper {
if (attribute.name === 'style') { if (attribute.name === 'style') {
return new StyleAttributeWrapper(this, block, attribute); return new StyleAttributeWrapper(this, block, attribute);
} }
if (attribute.type === 'Spread') {
return new SpreadAttributeWrapper(this, block, attribute);
}
return new AttributeWrapper(this, block, attribute); return new AttributeWrapper(this, block, attribute);
}); });
@ -418,7 +422,7 @@ export default class ElementWrapper extends Wrapper {
return x`@_document.createElementNS("${namespace}", "${name}")`; return x`@_document.createElementNS("${namespace}", "${name}")`;
} }
const is = this.attributes.find(attr => attr.node.name === 'is'); const is: AttributeWrapper = this.attributes.find(attr => attr.node.name === 'is') as any;
if (is) { if (is) {
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}`)})`;
} }
@ -664,25 +668,24 @@ export default class ElementWrapper extends Wrapper {
this.attributes this.attributes
.forEach(attr => { .forEach(attr => {
const condition = attr.node.dependencies.size > 0 const dependencies = attr.node.get_dependencies();
? block.renderer.dirty(Array.from(attr.node.dependencies))
const condition = dependencies.length > 0
? block.renderer.dirty(dependencies)
: null; : null;
if (attr.node.is_spread) { if (attr instanceof SpreadAttributeWrapper) {
const snippet = attr.node.expression.manipulate(block); const snippet = attr.node.expression.manipulate(block);
initial_props.push(snippet); initial_props.push(snippet);
updates.push(condition ? x`${condition} && ${snippet}` : snippet); updates.push(condition ? x`${condition} && ${snippet}` : snippet);
} else { } else {
const metadata = attr.get_metadata(); const name = attr.property_name || attr.name;
const name = attr.is_indirectly_bound_value() initial_props.push(x`{ ${name}: ${attr.get_init(block, attr.get_value(block))} }`);
? '__value' const snippet = x`{ ${name}: ${attr.should_cache ? attr.last : attr.get_value(block)} }`;
: (metadata && metadata.property_name) || fix_attribute_casing(attr.node.name);
const snippet = x`{ ${name}: ${attr.get_value(block)} }`;
initial_props.push(snippet);
updates.push(condition ? x`${condition} && ${snippet}` : snippet); updates.push(condition ? x`${attr.get_dom_update_conditions(block, condition)} && ${snippet}` : snippet);
} }
}); });

@ -468,7 +468,7 @@ export default class InlineComponentWrapper extends Wrapper {
${name} = null; ${name} = null;
} }
} else if (${switch_value}) { } else if (${switch_value}) {
${updates.length && b`${name}.$set(${name_changes});`} ${updates.length > 0 && b`${name}.$set(${name_changes});`}
} }
`); `);

@ -51,7 +51,7 @@ export default class RawMustacheTagWrapper extends Tag {
content => x`${html_tag}.p(${content})` content => x`${html_tag}.p(${content})`
); );
const update_anchor = in_head ? 'null' : needs_anchor ? html_anchor : this.next ? this.next.var : 'null'; const update_anchor = needs_anchor ? html_anchor : this.next ? this.next.var : 'null';
block.chunks.hydrate.push(b`${html_tag} = new @HtmlTag(${update_anchor});`); block.chunks.hydrate.push(b`${html_tag} = new @HtmlTag(${update_anchor});`);
block.chunks.mount.push(b`${html_tag}.m(${init}, ${parent_node || '#target'}, ${parent_node ? null : '#anchor'});`); block.chunks.mount.push(b`${html_tag}.m(${init}, ${parent_node || '#target'}, ${parent_node ? null : '#anchor'});`);

@ -7,6 +7,7 @@ import { b, p, x } from 'code-red';
import { sanitize } from '../../../utils/names'; import { sanitize } from '../../../utils/names';
import add_to_set from '../../utils/add_to_set'; import add_to_set from '../../utils/add_to_set';
import get_slot_data from '../../utils/get_slot_data'; import get_slot_data from '../../utils/get_slot_data';
import { is_reserved_keyword } from '../../utils/reserved_keywords';
import Expression from '../../nodes/shared/Expression'; import Expression from '../../nodes/shared/Expression';
import is_dynamic from './shared/is_dynamic'; import is_dynamic from './shared/is_dynamic';
import { Identifier, ObjectExpression } from 'estree'; import { Identifier, ObjectExpression } from 'estree';
@ -94,11 +95,7 @@ export default class SlotWrapper extends Wrapper {
} }
}); });
const dynamic_dependencies = Array.from(attribute.dependencies).filter(name => { const dynamic_dependencies = Array.from(attribute.dependencies).filter((name) => this.is_dependency_dynamic(name));
if (this.node.scope.is_let(name)) return true;
const variable = renderer.component.var_lookup.get(name);
return is_dynamic(variable);
});
if (dynamic_dependencies.length > 0) { if (dynamic_dependencies.length > 0) {
changes.properties.push(p`${attribute.name}: ${renderer.dirty(dynamic_dependencies)}`); changes.properties.push(p`${attribute.name}: ${renderer.dirty(dynamic_dependencies)}`);
@ -157,17 +154,10 @@ export default class SlotWrapper extends Wrapper {
b`@transition_out(${slot_or_fallback}, #local);` b`@transition_out(${slot_or_fallback}, #local);`
); );
const is_dependency_dynamic = name => { const dynamic_dependencies = Array.from(this.dependencies).filter((name) => this.is_dependency_dynamic(name));
if (name === '$$scope') return true;
if (this.node.scope.is_let(name)) return true;
const variable = renderer.component.var_lookup.get(name);
return is_dynamic(variable);
};
const dynamic_dependencies = Array.from(this.dependencies).filter(is_dependency_dynamic);
const fallback_dynamic_dependencies = has_fallback const fallback_dynamic_dependencies = has_fallback
? Array.from(this.fallback.dependencies).filter(is_dependency_dynamic) ? Array.from(this.fallback.dependencies).filter((name) => this.is_dependency_dynamic(name))
: []; : [];
const slot_update = b` const slot_update = b`
@ -201,4 +191,12 @@ export default class SlotWrapper extends Wrapper {
b`if (${slot_or_fallback}) ${slot_or_fallback}.d(detaching);` b`if (${slot_or_fallback}) ${slot_or_fallback}.d(detaching);`
); );
} }
is_dependency_dynamic(name: string) {
if (name === '$$scope') return true;
if (this.node.scope.is_let(name)) return true;
if (is_reserved_keyword(name)) return true;
const variable = this.renderer.component.var_lookup.get(name);
return is_dynamic(variable);
}
} }

@ -94,8 +94,8 @@ export default async function preprocess(
for (const fn of script) { for (const fn of script) {
source = await replace_async( source = await replace_async(
source, source,
/<!--[^]*?-->|<script(\s[^]*?)?>([^]*?)<\/script>/gi, /<!--[^]*?-->|<script(\s[^]*?)?(?:>([^]*?)<\/script>|\/>)/gi,
async (match, attributes = '', content) => { async (match, attributes = '', content = '') => {
if (!attributes && !content) { if (!attributes && !content) {
return match; return match;
} }
@ -114,8 +114,8 @@ export default async function preprocess(
for (const fn of style) { for (const fn of style) {
source = await replace_async( source = await replace_async(
source, source,
/<!--[^]*?-->|<style(\s[^]*?)?>([^]*?)<\/style>/gi, /<!--[^]*?-->|<style(\s[^]*?)?(?:>([^]*?)<\/style>|\/>)/gi,
async (match, attributes = '', content) => { async (match, attributes = '', content = '') => {
if (!attributes && !content) { if (!attributes && !content) {
return match; return match;
} }

@ -11,9 +11,9 @@ export interface AnimationConfig {
} }
interface FlipParams { interface FlipParams {
delay: number; delay?: number;
duration: number | ((len: number) => number); duration?: number | ((len: number) => number);
easing: (t: number) => number; easing?: (t: number) => number;
} }
export function flip(node: Element, animation: { from: DOMRect; to: DOMRect }, params: FlipParams): AnimationConfig { export function flip(node: Element, animation: { from: DOMRect; to: DOMRect }, params: FlipParams): AnimationConfig {

@ -73,7 +73,7 @@ export function dataset_dev(node: HTMLElement, property: string, value?: any) {
export function set_data_dev(text, data) { export function set_data_dev(text, data) {
data = '' + data; data = '' + data;
if (text.data === data) return; if (text.wholeText === data) return;
dispatch_dev("SvelteDOMSetData", { node: text, data }); dispatch_dev("SvelteDOMSetData", { node: text, data });
text.data = data; text.data = data;

@ -193,7 +193,7 @@ export function claim_space(nodes) {
export function set_data(text, data) { export function set_data(text, data) {
data = '' + data; data = '' + data;
if (text.data !== data) text.data = data; if (text.wholeText !== data) text.data = data;
} }
export function set_input_value(input, value) { export function set_input_value(input, value) {

@ -42,6 +42,10 @@ export function not_equal(a, b) {
return a != a ? b == b : a !== b; return a != a ? b == b : a !== b;
} }
export function is_empty(obj) {
return Object.keys(obj).length === 0;
}
export function validate_store(store, name) { export function validate_store(store, name) {
if (store != null && typeof store.subscribe !== 'function') { if (store != null && typeof store.subscribe !== 'function') {
throw new Error(`'${name}' is not a store with a 'subscribe' method`); throw new Error(`'${name}' is not a store with a 'subscribe' method`);

@ -12,11 +12,11 @@ export interface TransitionConfig {
} }
interface BlurParams { interface BlurParams {
delay: number; delay?: number;
duration: number; duration?: number;
easing?: EasingFunction; easing?: EasingFunction;
amount: number; amount?: number;
opacity: number; opacity?: number;
} }
export function blur(node: Element, { export function blur(node: Element, {
@ -41,9 +41,9 @@ export function blur(node: Element, {
} }
interface FadeParams { interface FadeParams {
delay: number; delay?: number;
duration: number; duration?: number;
easing: EasingFunction; easing?: EasingFunction;
} }
export function fade(node: Element, { export function fade(node: Element, {
@ -62,12 +62,12 @@ export function fade(node: Element, {
} }
interface FlyParams { interface FlyParams {
delay: number; delay?: number;
duration: number; duration?: number;
easing: EasingFunction; easing?: EasingFunction;
x: number; x?: number;
y: number; y?: number;
opacity: number; opacity?: number;
} }
export function fly(node: Element, { export function fly(node: Element, {
@ -95,9 +95,9 @@ export function fly(node: Element, {
} }
interface SlideParams { interface SlideParams {
delay: number; delay?: number;
duration: number; duration?: number;
easing: EasingFunction; easing?: EasingFunction;
} }
export function slide(node: Element, { export function slide(node: Element, {
@ -133,11 +133,11 @@ export function slide(node: Element, {
} }
interface ScaleParams { interface ScaleParams {
delay: number; delay?: number;
duration: number; duration?: number;
easing: EasingFunction; easing?: EasingFunction;
start: number; start?: number;
opacity: number; opacity?: number;
} }
export function scale(node: Element, { export function scale(node: Element, {
@ -166,10 +166,10 @@ export function scale(node: Element, {
} }
interface DrawParams { interface DrawParams {
delay: number; delay?: number;
speed: number; speed?: number;
duration: number | ((len: number) => number); duration?: number | ((len: number) => number);
easing: EasingFunction; easing?: EasingFunction;
} }
export function draw(node: SVGElement & { getTotalLength(): number }, { export function draw(node: SVGElement & { getTotalLength(): number }, {
@ -199,9 +199,9 @@ export function draw(node: SVGElement & { getTotalLength(): number }, {
} }
interface CrossfadeParams { interface CrossfadeParams {
delay: number; delay?: number;
duration: number | ((len: number) => number); duration?: number | ((len: number) => number);
easing: EasingFunction; easing?: EasingFunction;
} }
type ClientRectMap = Map<any, { rect: ClientRect }>; type ClientRectMap = Map<any, { rect: ClientRect }>;

@ -0,0 +1,12 @@
import * as assert from "assert";
export default {
preprocess: {
script: ({ content, attributes }) => {
assert.equal(content, "");
return {
code: `console.log("${attributes["the-answer"]}");`
};
}
}
};

@ -0,0 +1 @@
<script the-answer="42">console.log("42");</script>

@ -0,0 +1,12 @@
import * as assert from "assert";
export default {
preprocess: {
style: ({ content, attributes: { color } }) => {
assert.equal(content, "");
return {
code: `div { color: ${color}; }`
};
}
}
};

@ -0,0 +1,3 @@
<div class='brand-color'>$brand</div>
<style color="red"/>

@ -0,0 +1,3 @@
<div class='brand-color'>$brand</div>
<style color="red">div { color: red; }</style>

@ -0,0 +1,15 @@
export default {
html: `
<div contenteditable=""></div>
`,
async test({ assert, component, target, window }) {
const div = target.querySelector('div');
const text = window.document.createTextNode('a');
div.insertBefore(text, null);
const event = new window.InputEvent('input');
await div.dispatchEvent(event);
assert.equal(div.textContent, 'a');
}
};

@ -0,0 +1,6 @@
<script>
let text = '';
const updater = (event) => {text = event.target.textContent}
</script>
<div contenteditable on:input={updater}>{text}</div>

@ -0,0 +1,6 @@
<script>
export let id;
export let callback;
$: $$props, callback(id);
</script>

@ -0,0 +1,30 @@
let callbacks = [];
export default {
props: {
callback: (value) => callbacks.push(value),
val1: "1",
val2: "2",
},
before_test() {
callbacks = [];
},
async test({ assert, component, target }) {
assert.equal(callbacks.length, 2);
assert.equal(JSON.stringify(callbacks), '["1","2"]');
component.val1 = "3";
assert.equal(callbacks.length, 3);
assert.equal(JSON.stringify(callbacks), '["1","2","1"]');
component.val1 = "4";
assert.equal(callbacks.length, 4);
assert.equal(JSON.stringify(callbacks), '["1","2","1","1"]');
component.val2 = "5";
assert.equal(callbacks.length, 5);
assert.equal(JSON.stringify(callbacks), '["1","2","1","1","2"]');
},
};

@ -0,0 +1,8 @@
<script>
import Comp from './Comp.svelte';
export let callback;
export let val1, val2;
</script>
<Comp id="1" {callback} value={val1} />
<Comp id="2" {callback} value={val2} />

@ -0,0 +1,21 @@
export default {
html: `
<h1>hi</h1>
<button>Change</button>
`,
async test({ assert, component, target, window }) {
const btn = target.querySelector("button");
const clickEvent = new window.MouseEvent("click");
await btn.dispatchEvent(clickEvent);
assert.htmlEqual(
target.innerHTML,
`
<h1>changed</h1>
<button>Change</button>
`
);
},
};

@ -0,0 +1,13 @@
<script>
import Comp from './Comp.svelte'
let p = "hi"
</script>
<Comp someprop={p} let:props>
<h1>
{props.someprop}
</h1>
</Comp>
<button on:click={()=> p = "changed"}>Change
</button>

@ -1,33 +1,3 @@
export default { export default {
html: ` html: `<p>x<span>baz</span></p>`
<button>Switch</button>
<p>Another first line</p>
<p>This line should be last.</p>
`,
async test({ assert, target, window }) {
const btn = target.querySelector("button");
const clickEvent = new window.MouseEvent("click");
await btn.dispatchEvent(clickEvent);
assert.htmlEqual(
target.innerHTML,
`
<button>Switch</button>
<p>First line</p>
<p>This line should be last.</p>
`
);
await btn.dispatchEvent(clickEvent);
assert.htmlEqual(
target.innerHTML,
`
<button>Switch</button>
<p>Another first line</p>
<p>This line should be last.</p>
`
);
},
}; };

@ -1,17 +1 @@
<script> <p>{@html 'x'}<span>baz</span></p>
import Component from './Component.svelte';
let content1 = `<p>First line</p>`;
let content2 = `<p>Another first line</p>`
let show = false;
$: content = show ? content1 : content2;
</script>
<button on:click={() => show = !show}>
Switch
</button>
<Component>
{@html content}
</Component>

@ -0,0 +1,16 @@
export default {
async test({ assert, target, window }) {
const btn = target.querySelector("button");
const clickEvent = new window.MouseEvent("click");
assert.equal(window.document.head.innerHTML.includes('<style>body { color: blue; }</style><style>body { color: green; }</style>'), true);
await btn.dispatchEvent(clickEvent);
assert.equal(window.document.head.innerHTML.includes('<style>body { color: red; }</style><style>body { color: green; }</style>'), true);
await btn.dispatchEvent(clickEvent);
assert.equal(window.document.head.innerHTML.includes('<style>body { color: blue; }</style><style>body { color: green; }</style>'), true);
},
};

@ -0,0 +1,16 @@
<script>
let content1 = `<style>body { color: red; }</style>`;
let content2 = `<style>body { color: blue; }</style>`
let show = false;
$: content = show ? content1 : content2;
</script>
<button on:click={() => show = !show}>
Switch
</button>
<svelte:head>
{@html content}
<style>body { color: green; }</style>
</svelte:head>

@ -1,3 +1,33 @@
export default { export default {
html: `<p>x<span>baz</span></p>` html: `
<button>Switch</button>
<p>Another first line</p>
<p>This line should be last.</p>
`,
async test({ assert, target, window }) {
const btn = target.querySelector("button");
const clickEvent = new window.MouseEvent("click");
await btn.dispatchEvent(clickEvent);
assert.htmlEqual(
target.innerHTML,
`
<button>Switch</button>
<p>First line</p>
<p>This line should be last.</p>
`
);
await btn.dispatchEvent(clickEvent);
assert.htmlEqual(
target.innerHTML,
`
<button>Switch</button>
<p>Another first line</p>
<p>This line should be last.</p>
`
);
},
}; };

@ -1 +1,17 @@
<p>{@html 'x'}<span>baz</span></p> <script>
import Component from './Component.svelte';
let content1 = `<p>First line</p>`;
let content2 = `<p>Another first line</p>`
let show = false;
$: content = show ? content1 : content2;
</script>
<button on:click={() => show = !show}>
Switch
</button>
<Component>
{@html content}
</Component>

@ -0,0 +1,18 @@
<script>
import { omit } from './utils.js';
export let value;
function onInput(e) {
value = e.target.value;
}
$: props = omit($$props, 'value');
</script>
<input
type="text"
{...props}
on:input={onInput}
{value}
/>

@ -0,0 +1,19 @@
<script>
import { omit } from './utils.js';
export let value;
function onInput(e) {
value = e.target.value;
}
$: props = omit($$props, 'value', 'minlength');
</script>
<input
type="text"
minlength="10"
value={value}
{...props}
on:input={onInput}
/>

@ -0,0 +1,62 @@
export default {
async test({ assert, component, target, window }) {
const [input1, input2] = target.querySelectorAll("input");
// we are not able emulate user interaction in jsdom,
// therefore, jsdom could not validate minlength / maxlength
// we simulate user input with
// setting input.value + dispathEvent
// and we determine if svelte does not set the `input.value` again by
// spying on the setter of `input.value`
const spy1 = spyOnValueSetter(input1, input1.value);
const spy2 = spyOnValueSetter(input2, input2.value);
const event = new window.Event("input");
input1.value = '12345';
spy1.reset();
await input1.dispatchEvent(event);
assert.ok(!spy1.isSetCalled());
input2.value = '12345';
spy2.reset();
await input2.dispatchEvent(event);
assert.ok(!spy2.isSetCalled());
spy1.reset();
component.val1 = '56789';
assert.ok(spy1.isSetCalled());
spy2.reset();
component.val2 = '56789';
assert.ok(spy2.isSetCalled());
},
};
function spyOnValueSetter(input, initialValue) {
let value = initialValue;
let isSet = false;
Object.defineProperty(input, "value", {
get() {
return value;
},
set(_value) {
value = _value;
isSet = true;
},
});
return {
isSetCalled() {
return isSet;
},
reset() {
isSet = false;
},
};
}

@ -0,0 +1,9 @@
<script>
import InputOne from './InputOne.svelte';
import InputTwo from './InputTwo.svelte';
export let val1 = '';
export let val2 = '';
</script>
<InputOne bind:value={val1} required minlength="10" />
<InputTwo bind:value={val2} required minlength="10" />

@ -0,0 +1,6 @@
export function omit(obj, ...keysToOmit) {
return Object.keys(obj).reduce((acc, key) => {
if (keysToOmit.indexOf(key) === -1) acc[key] = obj[key];
return acc;
}, {});
}

@ -0,0 +1,3 @@
export default {
error: `Stores must be declared at the top level of the component (this may change in a future version of Svelte)`
};

@ -0,0 +1,9 @@
<script>
import { writable } from 'svelte/store';
const store = writable();
function foo() {
let store = 1;
$store = 2;
}
</script>

@ -0,0 +1,3 @@
export default {
error: `Stores must be declared at the top level of the component (this may change in a future version of Svelte)`
};

@ -0,0 +1,9 @@
<script>
import { writable } from 'svelte/store';
const store = writable();
</script>
<button
on:click={(store) => {
$store = Math.random();
}} />
Loading…
Cancel
Save