Merge remote-tracking branch 'upstream/master' into customStyleTag

pull/5639/head
Ivan Hofer 5 years ago
commit 4026fb00ec

@ -1,5 +1,12 @@
# Svelte changelog
## Unreleased
* Fix `$$props` and `$$restProps` when compiling to a custom element ([#5482](https://github.com/sveltejs/svelte/issues/5482))
* Fix function calls in `<slot>` props that use contextual values ([#5565](https://github.com/sveltejs/svelte/issues/5565))
* Fix handling aborted transitions in `{:else}` blocks ([#5573](https://github.com/sveltejs/svelte/issues/5573))
* Add `Element` and `Node` to known globals ([#5586](https://github.com/sveltejs/svelte/issues/5586))
## 3.29.4
* Fix code generation error with `??` alongside logical operators ([#5558](https://github.com/sveltejs/svelte/issues/5558))

@ -9,7 +9,7 @@
<img src="https://img.shields.io/npm/l/svelte.svg" alt="license">
</a>
<a href="https://svelte.dev/chat">
<img src="https://img.shields.io/badge/chat-on%20discord-7289da.svg" alt="Chat">
<img src="https://img.shields.io/discord/457912077277855764?label=chat&logo=discord" alt="Chat">
</a>
</p>

4
package-lock.json generated

@ -144,8 +144,8 @@
}
},
"@sveltejs/eslint-config": {
"version": "github:sveltejs/eslint-config#5d1ba28f99568e42f26d9b7484ab57720f6ec9b4",
"from": "github:sveltejs/eslint-config#v5.4.0",
"version": "github:sveltejs/eslint-config#54081d752d199dba97db9f578665c87f18469da3",
"from": "github:sveltejs/eslint-config#v5.5.0",
"dev": true
},
"@tootallnate/once": {

@ -63,7 +63,7 @@
"@rollup/plugin-sucrase": "^3.0.0",
"@rollup/plugin-typescript": "^2.0.1",
"@rollup/plugin-virtual": "^2.0.0",
"@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.4.0",
"@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.5.0",
"@types/mocha": "^7.0.0",
"@types/node": "^8.10.53",
"@typescript-eslint/eslint-plugin": "^3.0.2",

@ -0,0 +1,46 @@
---
title: What's new in Svelte: November 2020
description: Slot forwarding fixes, SvelteKit for faster local development, and more from Svelte Summit
author: Daniel Sandoval
authorURL: https://desandoval.net
---
Welcome back to the "What's new in Svelte" series! This month, we're covering new features & bug fixes, last month's Svelte Summit and some stand-out sites and libraries...
## New features & impactful bug fixes
1. Destructuring Promises now works as expected by using the `{#await}` syntax
(**3.29.3**, [Example](https://svelte.dev/repl/3fd4e2cecfa14d629961478f1dac2445?version=3.29.3))
2. Slot forwarding (released in 3.29.0) should no longer hang during compilation (**3.29.3**, [Example](https://svelte.dev/repl/29959e70103f4868a6525c0734934936?version=3.29.3))
3. Better typings for the `get` function in `svelte/store` and on lifecycle hooks (**3.29.1**)
**What's going on in Sapper?**
Sapper got some new types in its `preload` function, which will make typing easier if you are using TypeScript. See the [Sapper docs](https://sapper.svelte.dev/docs#Typing_the_function) on how to use them. There also were fixes to `preload` links in exported sites. Route layouts got a few fixes too - including ensuring CSS is applied to nested route layouts. You can also better organize your files now that extensions with multiple dots are supported. (**0.28.10**)
For all the features and bugfixes see the CHANGELOGs for [Svelte](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md) and [Sapper](https://github.com/sveltejs/sapper/blob/master/CHANGELOG.md).
## [Svelte Summit](https://sveltesummit.com/) was Svelte-tacular!
- Rich Harris demoed the possible future of Svelte development in a talk titled "Futuristic Web Development". The not-yet-public project is called SvelteKit (name may change) and will bring a first-class developer experience and more flexibility for build outputs. If you want to get the full sneak-peek, [check out the video](https://www.youtube.com/watch?v=qSfdtmcZ4d0).
- 17 speakers made the best of the conference's virtual format... From floating heads to seamless demos, Svelte developers from every skill level will find something of interest in this year's [YouTube playlist](https://www.youtube.com/playlist?list=PL8bMgX1kyZThM1sbYCoWdTcpiYysJsSeu)
---
## Community Showcase
- [Svelte Lab](https://sveltelab.app/) showcases a variety of components, visualizations and interactions that can be achieved in Svelte. You can click into any component to see its source or edit it, using the site's built-in REPL
- [svelte-electron-boilerplate](https://github.com/hjalmar/svelte-electron-boilerplate) is a fast way to get up and running with a Svelte app built in the desktop javascript framework, Electron
- [React Hooks in Svelte](https://github.com/joshnuss/react-hooks-in-svelte) showcases examples of common React Hooks ported to Svelte.
- [gurlic](https://gurlic.com/) is a social network and internet experiment that is super snappy thanks to Svelte
- [Interference 2020](https://interference2020.org/) visualizes reported foreign interference in the 2020 U.S. elections. You can learn more about how it was built in [YYY's talk at Svelte Summit]()
- [jitsi-svelte](https://github.com/relm-us/jitsi-svelte) lets you easily create your own custom Jitsi client by providing out-of-the-box components built with Svelte
- [Ellx](https://ellx.io/) is part spreadsheet, part notebook and part IDE. It's super smooth thanks to Svelte 😎
- [This New Zealand news site](https://www.nzherald.co.nz/nz/election-2020-latest-results-party-vote-electorate-vote-and-full-data/5CFVO4ENKNQDE3SICRRNPU5GZM/) breaks down the results of the 2020 Parliamentary elections using Svelte
- [Budibase](https://github.com/Budibase/budibase) is a no-code app builder, powered by Svelte
- [Svelt-yjs](https://github.com/relm-us/svelt-yjs) combines the collaborative, local-first technology of Yjs with the power of Svelte to enable multiple users across the internet to stay in sync.
- [tabler-icons-svelte](https://github.com/benflap/tabler-icons-svelte) is a Svelte wrapper for over 850 free MIT-licensed high-quality SVG icons for you to use in your web projects.
## See you next month!
Got an idea for something to add to the Showcase? Want to get involved more with Svelte? We're always looking for maintainers, contributors and fanatics... Check out the [Svelte Society](https://sveltesociety.dev/), [Reddit](https://www.reddit.com/r/sveltejs/) and [Discord](https://discord.com/invite/yy75DKs) to get involved!

@ -1317,6 +1317,31 @@ Named slots allow consumers to target specific areas. They can also have fallbac
</div>
```
#### [`$$slots`](slots_object)
---
`$$slots` is an object whose keys are the names of the slots passed into the component by the parent. If the parent does not pass in a slot with a particular name, that name will not be a present in `$$slots`. This allows components to render a slot (and other elements, like wrappers for styling) only if the parent provides it.
Note that explicitly passing in an empty named slot will add that slot's name to `$$slots`. For example, if a parent passes `<div slot="title" />` to a child component, `$$slots.title` will be truthy within the child.
```sv
<!-- App.svelte -->
<Card>
<h1 slot="title">Blog Post Title</h1>
</Card>
<!-- Card.svelte -->
<div>
<slot name="title"></slot>
{#if $$slots.description}
<!-- This slot and the <hr> before it will not render. -->
<hr>
<slot name="description"></slot>
{/if}
</div>
```
#### [`<slot let:`*name*`={`*value*`}>`](slot_let)
---

@ -115,8 +115,9 @@
on:mousedown={handleMousedown}
bind:currentTime={time}
bind:duration
bind:paused
></video>
bind:paused>
<track kind="captions"/>
</video>
<div class="controls" style="opacity: {duration && showControls ? 1 : 0}">
<progress value="{(time / duration) || 0}"/>

@ -0,0 +1,57 @@
<script>
import Project from './Project.svelte'
import Comment from './Comment.svelte'
</script>
<style>
h1 {
font-weight: 300;
margin: 0 1rem;
}
ul {
list-style: none;
padding: 0;
margin: 0.5rem;
display: flex;
}
@media (max-width: 600px) {
ul {
flex-direction: column;
}
}
li {
padding: 0.5rem;
flex: 1 1 50%;
min-width: 200px;
}
</style>
<h1>
Projects
</h1>
<ul>
<li>
<Project
title="Add Typescript support"
tasksCompleted={25}
totalTasks={57}
>
<div slot="comments">
<Comment name="Ecma Script" postedAt={new Date('2020-08-17T14:12:23')}>
<p>Those interface tests are now passing.</p>
</Comment>
</div>
</Project>
</li>
<li>
<Project
title="Update documentation"
tasksCompleted={18}
totalTasks={21}
/>
</li>
</ul>

@ -0,0 +1,56 @@
<script>
export let name;
export let postedAt;
$: avatar = `https://ui-avatars.com/api/?name=${name.replace(/ /g, '+')}&rounded=true&background=ff3e00&color=fff&bold=true`;
</script>
<style>
article {
background-color: #fff;
border: 1px #ccc solid;
border-radius: 4px;
padding: 1rem;
}
.header {
align-items: center;
display: flex;
}
.details {
flex: 1 1 auto;
margin-left: 0.5rem
}
h4 {
margin: 0;
}
time {
color: #777;
font-size: 0.75rem;
text-decoration: underline;
}
.body {
margin-top: 0.5rem;
}
.body :global(p) {
margin: 0;
}
</style>
<article>
<div class="header">
<img src={avatar} alt="" height="32" width="32">
<div class="details">
<h4>{name}</h4>
<time datetime={postedAt.toISOString()}>{postedAt.toLocaleDateString()}</time>
</div>
</div>
<div class="body">
<slot></slot>
</div>
</article>

@ -0,0 +1,62 @@
<script>
export let title;
export let tasksCompleted = 0;
export let totalTasks = 0;
</script>
<style>
article {
border: 1px #ccc solid;
border-radius: 4px;
position: relative;
}
article > div {
padding: 1.25rem;
}
article.has-discussion::after {
content: '';
background-color: #ff3e00;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
height: 20px;
position: absolute;
right: -10px;
top: -10px;
width: 20px;
}
h2,
h3 {
margin: 0 0 0.5rem;
}
h3 {
font-size: 0.875rem;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
}
p {
color: #777;
margin: 0;
}
.discussion {
background-color: #eee;
border-top: 1px #ccc solid;
}
</style>
<article class:has-discussion={true}>
<div>
<h2>{title}</h2>
<p>{tasksCompleted}/{totalTasks} tasks completed</p>
</div>
<div class="discussion">
<h3>Comments</h3>
<slot name="comments"></slot>
</div>
</article>

@ -0,0 +1,57 @@
<script>
import Project from './Project.svelte'
import Comment from './Comment.svelte'
</script>
<style>
h1 {
font-weight: 300;
margin: 0 1rem;
}
ul {
list-style: none;
padding: 0;
margin: 0.5rem;
display: flex;
}
@media (max-width: 600px) {
ul {
flex-direction: column;
}
}
li {
padding: 0.5rem;
flex: 1 1 50%;
min-width: 200px;
}
</style>
<h1>
Projects
</h1>
<ul>
<li>
<Project
title="Add Typescript support"
tasksCompleted={25}
totalTasks={57}
>
<div slot="comments">
<Comment name="Ecma Script" postedAt={new Date('2020-08-17T14:12:23')}>
<p>Those interface tests are now passing.</p>
</Comment>
</div>
</Project>
</li>
<li>
<Project
title="Update documentation"
tasksCompleted={18}
totalTasks={21}
/>
</li>
</ul>

@ -0,0 +1,56 @@
<script>
export let name;
export let postedAt;
$: avatar = `https://ui-avatars.com/api/?name=${name.replace(/ /g, '+')}&rounded=true&background=ff3e00&color=fff&bold=true`;
</script>
<style>
article {
background-color: #fff;
border: 1px #ccc solid;
border-radius: 4px;
padding: 1rem;
}
.header {
align-items: center;
display: flex;
}
.details {
flex: 1 1 auto;
margin-left: 0.5rem
}
h4 {
margin: 0;
}
time {
color: #777;
font-size: 0.75rem;
text-decoration: underline;
}
.body {
margin-top: 0.5rem;
}
.body :global(p) {
margin: 0;
}
</style>
<article>
<div class="header">
<img src={avatar} alt="" height="32" width="32">
<div class="details">
<h4>{name}</h4>
<time datetime={postedAt.toISOString()}>{postedAt.toLocaleDateString()}</time>
</div>
</div>
<div class="body">
<slot></slot>
</div>
</article>

@ -0,0 +1,64 @@
<script>
export let title;
export let tasksCompleted = 0;
export let totalTasks = 0;
</script>
<style>
article {
border: 1px #ccc solid;
border-radius: 4px;
position: relative;
}
article > div {
padding: 1.25rem;
}
article.has-discussion::after {
content: '';
background-color: #ff3e00;
border-radius: 10px;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
height: 20px;
position: absolute;
right: -10px;
top: -10px;
width: 20px;
}
h2,
h3 {
margin: 0 0 0.5rem;
}
h3 {
font-size: 0.875rem;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
}
p {
color: #777;
margin: 0;
}
.discussion {
background-color: #eee;
border-top: 1px #ccc solid;
}
</style>
<article class:has-discussion={$$slots.comments}>
<div>
<h2>{title}</h2>
<p>{tasksCompleted}/{totalTasks} tasks completed</p>
</div>
{#if $$slots.comments}
<div class="discussion">
<h3>Comments</h3>
<slot name="comments"></slot>
</div>
{/if}
</article>

@ -0,0 +1,28 @@
---
title: Checking for slot content
---
In some cases, you may want to control parts of your component based on whether the parent passes in content for a certain slot. Perhaps you have a wrapper around that slot, and you don't want to render it if the slot is empty. Or perhaps you'd like to apply a class only if the slot is present. You can do this by checking the properties of the special `$$slots` variable.
`$$slots` is an object whose keys are the names of the slots passed in by the parent component. If the parent leaves a slot empty, then `$$slots` will not have an entry for that slot.
Notice that both instances of `<Project>` in this example render a container for comments and a notification dot, even though only one has comments. We want to use `$$slots` to make sure we only render these elements when the parent `<App>` passes in content for the `comments` slot.
In `Project.svelte`, update the `class:has-discussion` directive on the `<article>`:
```html
<article class:has-discussion={$$slots.comments}>
```
Next, wrap the `comments` slot and its wrapping `<div>` in an `if` block that checks `$$slots`:
```html
{#if $$slots.comments}
<div class="discussion">
<h3>Comments</h3>
<slot name="comments"></slot>
</div>
{/if}
```
Now the comments container and the notification dot won't render when `<App>` leaves the `comments` slot empty.

@ -521,8 +521,7 @@ export default class Component {
if (this.hoistable_nodes.has(node)) return false;
if (this.reactive_declaration_nodes.has(node)) return false;
if (node.type === 'ImportDeclaration') return false;
if (node.type === 'ExportDeclaration' && node.specifiers.length > 0)
return false;
if (node.type === 'ExportDeclaration' && node.specifiers.length > 0) return false;
return true;
});
}
@ -1038,8 +1037,9 @@ export default class Component {
this.vars.find(
variable => variable.name === name && variable.module
)
)
) {
return false;
}
return true;
});
@ -1288,8 +1288,9 @@ export default class Component {
declaration.dependencies.forEach(name => {
if (declaration.assignees.has(name)) return;
const earlier_declarations = lookup.get(name);
if (earlier_declarations)
if (earlier_declarations) {
earlier_declarations.forEach(add_declaration);
}
});
this.reactive_declarations.push(declaration);
@ -1319,8 +1320,9 @@ export default class Component {
if (globals.has(name) && node.type !== 'InlineComponent') return;
let message = `'${name}' is not defined`;
if (!this.ast.instance)
if (!this.ast.instance) {
message += `. Consider adding a <script> block with 'export let ${name}' to declare a prop`;
}
this.warn(node, {
code: 'missing-declaration',
@ -1382,8 +1384,9 @@ function process_component_options(component: Component, nodes) {
const message = "'tag' must be a string literal";
const tag = get_value(attribute, code, message);
if (typeof tag !== 'string' && tag !== null)
if (typeof tag !== 'string' && tag !== null) {
component.error(attribute, { code, message });
}
if (tag && !/^[a-zA-Z][a-zA-Z0-9]*-[a-zA-Z0-9-]+$/.test(tag)) {
component.error(attribute, {
@ -1408,8 +1411,9 @@ function process_component_options(component: Component, nodes) {
const message = "The 'namespace' attribute must be a string literal representing a valid namespace";
const ns = get_value(attribute, code, message);
if (typeof ns !== 'string')
if (typeof ns !== 'string') {
component.error(attribute, { code, message });
}
if (valid_namespaces.indexOf(ns) === -1) {
const match = fuzzymatch(ns, valid_namespaces);
@ -1437,8 +1441,9 @@ function process_component_options(component: Component, nodes) {
const message = `${name} attribute must be true or false`;
const value = get_value(attribute, code, message);
if (typeof value !== 'boolean')
if (typeof value !== 'boolean') {
component.error(attribute, { code, message });
}
component_options[name] = value;
break;

@ -67,17 +67,21 @@ export default class Binding extends Node {
} else {
const variable = component.var_lookup.get(name);
if (!variable || variable.global) component.error(this.expression.node, {
code: 'binding-undeclared',
message: `${name} is not declared`
});
if (!variable || variable.global) {
component.error(this.expression.node, {
code: 'binding-undeclared',
message: `${name} is not declared`
});
}
variable[this.expression.node.type === 'MemberExpression' ? 'mutated' : 'reassigned'] = true;
if (info.expression.type === 'Identifier' && !variable.writable) component.error(this.expression.node, {
code: 'invalid-binding',
message: 'Cannot bind to a variable which is not writable'
});
if (info.expression.type === 'Identifier' && !variable.writable) {
component.error(this.expression.node, {
code: 'invalid-binding',
message: 'Cannot bind to a variable which is not writable'
});
}
}
const type = parent.get_static_attribute_value('type');

@ -4,7 +4,6 @@ import is_reference from 'is-reference';
import flatten_reference from '../../utils/flatten_reference';
import { create_scopes, Scope, extract_names } from '../../utils/scope';
import { sanitize } from '../../../utils/names';
import Wrapper from '../../render_dom/wrappers/shared/Wrapper';
import TemplateScope from './TemplateScope';
import get_object from '../../utils/get_object';
import Block from '../../render_dom/Block';
@ -12,12 +11,12 @@ import is_dynamic from '../../render_dom/wrappers/shared/is_dynamic';
import { b } from 'code-red';
import { invalidate } from '../../render_dom/invalidate';
import { Node, FunctionExpression, Identifier } from 'estree';
import { TemplateNode } from '../../../interfaces';
import { INode } from '../interfaces';
import { is_reserved_keyword } from '../../utils/reserved_keywords';
import replace_object from '../../utils/replace_object';
import EachBlock from '../EachBlock';
type Owner = Wrapper | TemplateNode;
type Owner = INode;
export default class Expression {
type: 'Expression' = 'Expression';
@ -37,7 +36,6 @@ export default class Expression {
manipulated: Node;
// todo: owner type
constructor(component: Component, owner: Owner, template_scope: TemplateScope, info, lazy?: boolean) {
// TODO revert to direct property access in prod?
Object.defineProperties(this, {
@ -276,10 +274,12 @@ export default class Expression {
else {
// we need a combo block/init recipe
const deps = Array.from(contextual_dependencies);
const function_expression = node as FunctionExpression;
(node as FunctionExpression).params = [
const has_args = function_expression.params.length > 0;
function_expression.params = [
...deps.map(name => ({ type: 'Identifier', name } as Identifier)),
...(node as FunctionExpression).params
...function_expression.params
];
const context_args = deps.map(name => block.renderer.reference(name));
@ -291,18 +291,49 @@ export default class Expression {
this.replace(id as any);
if ((node as FunctionExpression).params.length > 0) {
declarations.push(b`
function ${id}(...args) {
return ${callee}(${context_args}, ...args);
}
`);
const func_declaration = has_args
? b`function ${id}(...args) {
return ${callee}(${context_args}, ...args);
}`
: b`function ${id}() {
return ${callee}(${context_args});
}`;
if (owner.type === 'Attribute' && owner.parent.name === 'slot') {
const dep_scopes = new Set<INode>(deps.map(name => template_scope.get_owner(name)));
// find the nearest scopes
let node: INode = owner.parent;
while (node && !dep_scopes.has(node)) {
node = node.parent;
}
const func_expression = func_declaration[0];
if (node.type === 'InlineComponent') {
// <Comp let:data />
this.replace(func_expression);
} else {
// {#each}, {#await}
const func_id = component.get_unique_name(id.name + '_func');
block.renderer.add_to_context(func_id.name, true);
// rename #ctx -> child_ctx;
walk(func_expression, {
enter(node) {
if (node.type === 'Identifier' && node.name === '#ctx') {
node.name = 'child_ctx';
}
}
});
// add to get_xxx_context
// child_ctx[x] = function () { ... }
(template_scope.get_owner(deps[0]) as EachBlock).contexts.push({
key: func_id,
modifier: () => func_expression
});
this.replace(block.renderer.reference(func_id));
}
} else {
declarations.push(b`
function ${id}() {
return ${callee}(${context_args});
}
`);
declarations.push(func_declaration);
}
}

@ -477,7 +477,7 @@ export default function dom(
${css.code && b`this.shadowRoot.innerHTML = \`<style>${css.code.replace(/\\/g, '\\\\')}${options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}</style>\`;`}
@init(this, { target: this.shadowRoot }, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, null, ${dirty});
@init(this, { target: this.shadowRoot, props: @attribute_to_object(this.attributes) }, ${definition}, ${has_create_fragment ? 'create_fragment': 'null'}, ${not_equal}, ${prop_indexes}, null, ${dirty});
${dev_props_check}

@ -196,11 +196,6 @@ export default class EachBlockWrapper extends Wrapper {
? !this.next.is_dom_node() :
!parent_node || !this.parent.is_dom_node();
this.context_props = this.node.contexts.map(prop => b`child_ctx[${renderer.context_lookup.get(prop.key.name).index}] = ${prop.modifier(x`list[i]`)};`);
if (this.node.has_binding) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.vars.each_block_value.name).index}] = list;`);
if (this.node.has_binding || this.node.has_index_binding || this.node.index) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.index_name.name).index}] = i;`);
const snippet = this.node.expression.manipulate(block);
block.chunks.init.push(b`let ${this.vars.each_block_value} = ${snippet};`);
@ -208,15 +203,6 @@ export default class EachBlockWrapper extends Wrapper {
block.chunks.init.push(b`@validate_each_argument(${this.vars.each_block_value});`);
}
// TODO which is better — Object.create(array) or array.slice()?
renderer.blocks.push(b`
function ${this.vars.get_each_context}(#ctx, list, i) {
const child_ctx = #ctx.slice();
${this.context_props}
return child_ctx;
}
`);
const initial_anchor_node: Identifier = { type: 'Identifier', name: parent_node ? 'null' : '#anchor' };
const initial_mount_node: Identifier = parent_node || { type: 'Identifier', name: '#target' };
const update_anchor_node = needs_anchor
@ -360,6 +346,19 @@ export default class EachBlockWrapper extends Wrapper {
if (this.else) {
this.else.fragment.render(this.else.block, null, x`#nodes` as Identifier);
}
this.context_props = this.node.contexts.map(prop => b`child_ctx[${renderer.context_lookup.get(prop.key.name).index}] = ${prop.modifier(x`list[i]`)};`);
if (this.node.has_binding) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.vars.each_block_value.name).index}] = list;`);
if (this.node.has_binding || this.node.has_index_binding || this.node.index) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.index_name.name).index}] = i;`);
// TODO which is better — Object.create(array) or array.slice()?
renderer.blocks.push(b`
function ${this.vars.get_each_context}(#ctx, list, i) {
const child_ctx = #ctx.slice();
${this.context_props}
return child_ctx;
}
`);
}
render_keyed({

@ -48,9 +48,10 @@ export default class AttributeWrapper extends BaseAttributeWrapper {
// special case — <option value={foo}> — see below
if (this.parent.node.name === 'option' && node.name === 'value') {
let select: ElementWrapper = this.parent;
while (select && (select.node.type !== 'Element' || select.node.name !== 'select'))
while (select && (select.node.type !== 'Element' || select.node.name !== 'select')) {
// @ts-ignore todo: doublecheck this, but looks to be correct
select = select.parent;
}
if (select && select.select_binding_dependencies) {
select.select_binding_dependencies.forEach(prop => {

@ -447,6 +447,8 @@ export default class IfBlockWrapper extends Wrapper {
if (!${name}) {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#ctx);
${name}.c();
} else {
${name}.p(#ctx, #dirty);
}
${has_transitions && b`@transition_in(${name}, 1);`}
${name}.m(${update_mount_node}, ${anchor});

@ -178,11 +178,12 @@ export class Parser {
}
read_until(pattern: RegExp) {
if (this.index >= this.template.length)
if (this.index >= this.template.length) {
this.error({
code: 'unexpected-eof',
message: 'Unexpected end of input'
});
}
const start = this.index;
const match = pattern.exec(this.template.slice(start));

@ -32,10 +32,12 @@ export default function read_script(parser: Parser, start: number, attributes: N
const script_start = parser.index;
const script_end = parser.template.indexOf(script_closing_tag, script_start);
if (script_end === -1) parser.error({
code: 'unclosed-script',
message: '<script> must have a closing tag'
});
if (script_end === -1) {
parser.error({
code: 'unclosed-script',
message: '<script> must have a closing tag'
});
}
const source = parser.template.slice(0, script_start).replace(/[^\n]/g, ' ') +
parser.template.slice(script_start, script_end);

@ -288,10 +288,12 @@ export default function mustache(parser: Parser) {
if (parser.eat(',')) {
parser.allow_whitespace();
block.index = parser.read_identifier();
if (!block.index) parser.error({
code: 'expected-name',
message: 'Expected name'
});
if (!block.index) {
parser.error({
code: 'expected-name',
message: 'Expected name'
});
}
parser.allow_whitespace();
}

@ -13,8 +13,9 @@ const GRAM_SIZE_UPPER = 3;
// return an edit distance from 0 to 1
function _distance(str1: string, str2: string) {
if (str1 === null && str2 === null)
if (str1 === null && str2 === null) {
throw 'Trying to compare two null values';
}
if (str1 === null || str2 === null) return 0;
str1 = String(str1);
str2 = String(str2);

@ -13,6 +13,7 @@ export const globals = new Set([
'decodeURI',
'decodeURIComponent',
'document',
'Element',
'encodeURI',
'encodeURIComponent',
'Error',
@ -36,6 +37,7 @@ export const globals = new Set([
'NaN',
'navigator',
'Number',
'Node',
'Object',
'parseFloat',
'parseInt',

@ -360,3 +360,11 @@ export class HtmlTag {
this.n.forEach(detach);
}
}
export function attribute_to_object(attributes) {
const result = {};
for (const attribute of attributes) {
result[attribute.name] = attribute.value;
}
return result;
}

@ -34,9 +34,10 @@ function tick_spring<T>(ctx: TickContext<T>, last_value: T, current_value: T, ta
tick_spring(ctx, last_value[i], current_value[i], target_value[i]));
} else if (typeof current_value === 'object') {
const next_value = {};
for (const k in current_value)
for (const k in current_value) {
// @ts-ignore
next_value[k] = tick_spring(ctx, last_value[k], current_value[k], target_value[k]);
}
// @ts-ignore
return next_value;
} else {
@ -121,8 +122,9 @@ export function spring<T=any>(value?: T, opts: SpringOpts = {}): Spring<T> {
last_value = value;
store.set(value = next_value);
if (ctx.settled)
if (ctx.settled) {
task = null;
}
return !ctx.settled;
});
}

@ -31,3 +31,24 @@ export function equal(a, b, message) {
export function ok(condition, message) {
if (!condition) throw new Error(message || `Expected ${condition} to be truthy`);
}
export function htmlEqual(actual, expected, message) {
return deepEqual(
normalizeHtml(window, actual),
normalizeHtml(window, expected),
message
);
}
function normalizeHtml(window, html) {
try {
const node = window.document.createElement('div');
node.innerHTML = html
.replace(/<!--.*?-->/g, '')
.replace(/>[\s\r\n]+</g, '><')
.trim();
return node.innerHTML.replace(/<\/?noscript\/?>/g, '');
} catch (err) {
throw new Error(`Failed to normalize HTML:\n${html}`);
}
}

@ -0,0 +1,10 @@
<svelte:options tag="custom-element"/>
<script>
export let name;
</script>
<p>name: {name}</p>
<p>$$props: {JSON.stringify($$props)}</p>
<p>$$restProps: {JSON.stringify($$restProps)}</p>

@ -0,0 +1,13 @@
import * as assert from 'assert';
import './main.svelte';
export default function (target) {
target.innerHTML = '<custom-element name="world" answer="42" test="svelte"></custom-element>';
const el = target.querySelector('custom-element');
assert.htmlEqual(el.shadowRoot.innerHTML, `
<p>name: world</p>
<p>$$props: {"name":"world","answer":"42","test":"svelte"}</p>
<p>$$restProps: {"answer":"42","test":"svelte"}</p>
`);
}

@ -118,9 +118,11 @@ describe('hydration', () => {
throw err;
}
if (config.show) showOutput(cwd, {
hydratable: true
});
if (config.show) {
showOutput(cwd, {
hydratable: true
});
}
});
}

@ -1,6 +1,7 @@
/* generated by Svelte vX.Y.Z */
import {
SvelteElement,
attribute_to_object,
detach,
element,
init,
@ -34,7 +35,18 @@ class Component extends SvelteElement {
constructor(options) {
super();
this.shadowRoot.innerHTML = `<style>div{animation:foo 1s}@keyframes foo{0%{opacity:0}100%{opacity:1}}</style>`;
init(this, { target: this.shadowRoot }, null, create_fragment, safe_not_equal, {});
init(
this,
{
target: this.shadowRoot,
props: attribute_to_object(this.attributes)
},
null,
create_fragment,
safe_not_equal,
{}
);
if (options) {
if (options.target) {

@ -0,0 +1,14 @@
<script>
let keys = ['a', 'b'];
let items = ['c', 'd'];
export let log;
function setKey(key, value, item) {
log.push(`setKey(${key}, ${value}, ${item})`);
}
</script>
{#each items as item (item)}
{#each keys as key (key)}
<slot {key} {item} set={(value) => setKey(key, value, item)} />
{/each}
{/each}

@ -0,0 +1,36 @@
export default {
html: `
<button type="button">Set a-c</button>
<button type="button">Set b-c</button>
<button type="button">Set a-d</button>
<button type="button">Set b-d</button>
`,
async test({ assert, target, window, component }) {
const [btn1, btn2, btn3, btn4] = target.querySelectorAll('button');
const click = new window.MouseEvent('click');
await btn1.dispatchEvent(click);
assert.deepEqual(component.log, ['setKey(a, value-a-c, c)']);
await btn2.dispatchEvent(click);
assert.deepEqual(component.log, [
'setKey(a, value-a-c, c)',
'setKey(b, value-b-c, c)'
]);
await btn3.dispatchEvent(click);
assert.deepEqual(component.log, [
'setKey(a, value-a-c, c)',
'setKey(b, value-b-c, c)',
'setKey(a, value-a-d, d)'
]);
await btn4.dispatchEvent(click);
assert.deepEqual(component.log, [
'setKey(a, value-a-c, c)',
'setKey(b, value-b-c, c)',
'setKey(a, value-a-d, d)',
'setKey(b, value-b-d, d)'
]);
}
};

@ -0,0 +1,8 @@
<script>
import Nested from './Nested.svelte';
export let log = [];
</script>
<Nested {log} let:set let:key let:item>
<button type="button" on:click={() => set(`value-${key}-${item}`)}>Set {key}-{item}</button>
</Nested>

@ -0,0 +1,11 @@
<script>
let keys = ['a', 'b'];
export let log;
function setKey(key, value) {
log.push(`setKey(${key}, ${value})`);
}
</script>
{#each keys as key (key)}
<slot {key} set={(value) => setKey(key, value)} />
{/each}

@ -0,0 +1,19 @@
export default {
html: `
<button type="button">Set a</button>
<button type="button">Set b</button>
`,
async test({ assert, target, window, component }) {
const [btn1, btn2] = target.querySelectorAll('button');
const click = new window.MouseEvent('click');
await btn1.dispatchEvent(click);
assert.deepEqual(component.log, ['setKey(a, value-a)']);
await btn2.dispatchEvent(click);
assert.deepEqual(component.log, [
'setKey(a, value-a)',
'setKey(b, value-b)'
]);
}
};

@ -0,0 +1,8 @@
<script>
import Nested from './Nested.svelte';
export let log = [];
</script>
<Nested {log} let:set let:key>
<button type="button" on:click={() => set(`value-${key}`)}>Set {key}</button>
</Nested>

@ -0,0 +1,9 @@
<script>
export let log;
function setKey(key, value) {
log.push(`setKey(${key}, ${value})`);
}
</script>
<slot key="a" set={setKey} />
<slot key="b" set={setKey} />

@ -0,0 +1,8 @@
<script>
import Inner from './Inner.svelte';
export let log;
</script>
<Inner {log} let:key let:set>
<slot {key} set={(value) => set(key, value)} />
</Inner>

@ -0,0 +1,19 @@
export default {
html: `
<button type="button">Set a</button>
<button type="button">Set b</button>
`,
async test({ assert, target, window, component }) {
const [btn1, btn2] = target.querySelectorAll('button');
const click = new window.MouseEvent('click');
await btn1.dispatchEvent(click);
assert.deepEqual(component.log, ['setKey(a, value-a)']);
await btn2.dispatchEvent(click);
assert.deepEqual(component.log, [
'setKey(a, value-a)',
'setKey(b, value-b)'
]);
}
};

@ -0,0 +1,8 @@
<script>
import Nested from './Nested.svelte';
export let log = [];
</script>
<Nested {log} let:set let:key>
<button type="button" on:click={() => set(`value-${key}`)}>Set {key}</button>
</Nested>

@ -0,0 +1,31 @@
// expect aborting halfway through outro transition
// to behave the same in `{#if}` block as in `{:else}` block
export default {
html: `
<div>a</div>
<div>a</div>
`,
async test({ assert, component, target, window, raf }) {
component.visible = false;
// abort halfway through the outro transition
raf.tick(50);
await component.$set({
visible: true,
array: ['a', 'b', 'c']
});
assert.htmlEqual(target.innerHTML, `
<div>a</div>
<div>b</div>
<div>c</div>
<div>a</div>
<div>b</div>
<div>c</div>
`);
}
};

@ -0,0 +1,21 @@
<script>
export let array = ['a'];
export let visible = true;
function slide(_, params) {
return params;
}
</script>
{#if visible}
{#each array as item}
<div transition:slide={{duration:100}}>{item}</div>
{/each}
{/if}
{#if !visible}
{:else}
{#each array as item}
<div transition:slide={{duration:100}}>{item}</div>
{/each}
{/if}
Loading…
Cancel
Save