Merge branch 'master' into pre2

pull/7280/head
Simon 4 years ago
commit e925e38eb7

@ -3,6 +3,10 @@
## Unreleased ## Unreleased
* Return the context object in `setContext` [#7427](https://github.com/sveltejs/svelte/issues/7427) * Return the context object in `setContext` [#7427](https://github.com/sveltejs/svelte/issues/7427)
* Fix `{@const}` tag not working inside Component when there's no `let:` [#7189](https://github.com/sveltejs/svelte/issues/7189)
* Ignore comments in `{#each}` blocks when containing elements with `animate:` ([#3999](https://github.com/sveltejs/svelte/issues/3999))
* Add a third parameter to the returned function of `createEventDispatcher` that allows passing an object of `{ cancelable: true }` to create a cancelable custom event. The returned function when called will also return a boolean depending on whether the event is cancelled ([#7064](https://github.com/sveltejs/svelte/pull/7064))
* Fix value of `let:` bindings not updating in certain cases ([#7440](https://github.com/sveltejs/svelte/issues/7440))
## 3.47.0 ## 3.47.0

34
package-lock.json generated

@ -2989,9 +2989,9 @@
"dev": true "dev": true
}, },
"node_modules/json-schema": { "node_modules/json-schema": {
"version": "0.2.3", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"dev": true "dev": true
}, },
"node_modules/json-schema-traverse": { "node_modules/json-schema-traverse": {
@ -3025,18 +3025,18 @@
} }
}, },
"node_modules/jsprim": { "node_modules/jsprim": {
"version": "1.4.1", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
"integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==",
"dev": true, "dev": true,
"engines": [
"node >=0.6.0"
],
"dependencies": { "dependencies": {
"assert-plus": "1.0.0", "assert-plus": "1.0.0",
"extsprintf": "1.3.0", "extsprintf": "1.3.0",
"json-schema": "0.2.3", "json-schema": "0.4.0",
"verror": "1.10.0" "verror": "1.10.0"
},
"engines": {
"node": ">=0.6.0"
} }
}, },
"node_modules/kleur": { "node_modules/kleur": {
@ -7426,9 +7426,9 @@
"dev": true "dev": true
}, },
"json-schema": { "json-schema": {
"version": "0.2.3", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"dev": true "dev": true
}, },
"json-schema-traverse": { "json-schema-traverse": {
@ -7459,14 +7459,14 @@
} }
}, },
"jsprim": { "jsprim": {
"version": "1.4.1", "version": "1.4.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
"integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==",
"dev": true, "dev": true,
"requires": { "requires": {
"assert-plus": "1.0.0", "assert-plus": "1.0.0",
"extsprintf": "1.3.0", "extsprintf": "1.3.0",
"json-schema": "0.2.3", "json-schema": "0.4.0",
"verror": "1.10.0" "verror": "1.10.0"
} }
}, },

@ -221,14 +221,14 @@ Retrieves the whole context map that belongs to the closest parent component. Mu
#### `createEventDispatcher` #### `createEventDispatcher`
```js ```js
dispatch: ((name: string, detail?: any) => void) = createEventDispatcher(); dispatch: ((name: string, detail?: any, options?: DispatchOptions) => boolean) = createEventDispatcher();
``` ```
--- ---
Creates an event dispatcher that can be used to dispatch [component events](/docs#template-syntax-component-directives-on-eventname). Event dispatchers are functions that can take two arguments: `name` and `detail`. Creates an event dispatcher that can be used to dispatch [component events](/docs#template-syntax-component-directives-on-eventname). Event dispatchers are functions that can take two arguments: `name` and `detail`.
Component events created with `createEventDispatcher` create a [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent). These events do not [bubble](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#Event_bubbling_and_capture) and are not cancellable with `event.preventDefault()`. The `detail` argument corresponds to the [CustomEvent.detail](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail) property and can contain any type of data. Component events created with `createEventDispatcher` create a [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent). These events do not [bubble](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#Event_bubbling_and_capture). The `detail` argument corresponds to the [CustomEvent.detail](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail) property and can contain any type of data.
```sv ```sv
<script> <script>
@ -254,6 +254,27 @@ Events dispatched from child components can be listened to in their parent. Any
<Child on:notify="{callbackFunction}"/> <Child on:notify="{callbackFunction}"/>
``` ```
---
Events can be cancelable by passing a third parameter to the dispatch function. The function returns `false` if the event is cancelled with `event.preventDefault()`, otherwise it returns `true`.
```sv
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function notify() {
const shouldContinue = dispatch('notify', 'detail value', { cancelable: true });
if (shouldContinue) {
// no one called preventDefault
} else {
// a listener called preventDefault
}
}
</script>
```
### `svelte/store` ### `svelte/store`
The `svelte/store` module exports functions for creating [readable](/docs#run-time-svelte-store-readable), [writable](/docs#run-time-svelte-store-writable) and [derived](/docs#run-time-svelte-store-derived) stores. The `svelte/store` module exports functions for creating [readable](/docs#run-time-svelte-store-readable), [writable](/docs#run-time-svelte-store-writable) and [derived](/docs#run-time-svelte-store-derived) stores.

@ -2,4 +2,4 @@
question: How do I do hot module reloading? question: How do I do hot module reloading?
--- ---
We recommend using [SvelteKit](https://kit.svelte.dev/), which supports HMR out of the box and is built on top of Vite and svelte-hmr. There are also community plugins for [rollup](https://github.com/rixo/rollup-plugin-svelte-hot) and [webpack](https://github.com/rixo/svelte-loader-hot). We recommend using [SvelteKit](https://kit.svelte.dev/), which supports HMR out of the box and is built on top of [Vite](https://vitejs.dev/) and [svelte-hmr](https://github.com/sveltejs/svelte-hmr). There are also community plugins for [rollup](https://github.com/rixo/rollup-plugin-svelte-hot) and [webpack](https://github.com/rixo/svelte-loader-hot).

@ -0,0 +1,5 @@
---
question: Are there any third-party resources?
---
Svelte Society maintains a [list of books and videos](https://sveltesociety.dev/resources).

@ -1,18 +0,0 @@
---
question: Are there any video courses?
---
Rich Harris, the creator of Svelte, taught a course:
- [Frontend Masters](https://frontendmasters.com/courses/svelte/)
There are also a number of third-party courses:
- [Egghead](https://egghead.io/browse/frameworks/svelte)
- [Udemy](https://www.udemy.com/courses/search/?q=sveltejs+svelte) (Note: Udemy frequently has discounts over 90%)
- [Pluralsight](https://www.pluralsight.com/search?q=svelte)
Finally, there are also YouTube channels and playlists that teach Svelte:
- [Svelte Master](https://www.youtube.com/channel/UCg6SQd5jnWo5Y70rZD9SQFA)
- [Svelte Tutorial for Beginners](https://www.youtube.com/watch?v=zojEMeQGGHs&list=PL4cUxeGkcC9hlbrVO_2QFVqVPhlZmz7tO) by The Net Ninja

@ -1,9 +0,0 @@
---
question: Are there any books?
---
There are a few books:
- [Svelte Handbook](https://flaviocopes.com/page/svelte-handbook/) by Flavio Copes
- [Svelte 3 Up and Running](https://www.amazon.com/dp/B08D6T6BKS/) by Alessandro Segala
- [Svelte and Sapper in Action](https://www.manning.com/books/svelte-and-sapper-in-action) by R. Mark Volkmann

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

@ -0,0 +1,15 @@
<script>
let bgOpacity = 0.5;
$: color = bgOpacity < 0.6 ? '#000' : '#fff';
</script>
<input type="range" min="0" max="1" step="0.1" bind:value={bgOpacity} />
<p>This is a paragraph.</p>
<style>
p {
font-family: "Comic Sans MS", cursive;
background: rgba(255, 62, 0, var(--opacity));
}
</style>

@ -0,0 +1,15 @@
<script>
let bgOpacity = 0.5;
$: color = bgOpacity < 0.6 ? '#000' : '#fff';
</script>
<input type="range" min="0" max="1" step="0.1" bind:value={bgOpacity} />
<p style="color: {color}; --opacity: {bgOpacity};">This is a paragraph.</p>
<style>
p {
font-family: "Comic Sans MS", cursive;
background: rgba(255, 62, 0, var(--opacity));
}
</style>

@ -0,0 +1,10 @@
---
title: Inline styles
---
Apart from adding styles inside style tags, you can also add styles to individual elements using the style attribute. Usually you will want to do styling through CSS, but this can come in handy for dynamic styles, especially when combined with CSS custom properties.
Add the following style attribute to the paragraph element:
`style="color: {color}; --opacity: {bgOpacity};"`
Great, now you can style the paragraph using variables that change based on your input without having to make a class for every possible value.

@ -0,0 +1,15 @@
<script>
let bgOpacity = 0.5;
$: color = bgOpacity < 0.6 ? '#000' : '#fff';
</script>
<input type="range" min="0" max="1" step="0.1" bind:value={bgOpacity} />
<p style="color: {color}; --opacity: {bgOpacity};">This is a paragraph.</p>
<style>
p {
font-family: "Comic Sans MS", cursive;
background: rgba(255, 62, 0, var(--opacity));
}
</style>

@ -0,0 +1,15 @@
<script>
let bgOpacity = 0.5;
$: color = bgOpacity < 0.6 ? "#000" : "#fff";
</script>
<input type="range" min="0" max="1" step="0.1" bind:value={bgOpacity} />
<p style:color style:--opacity={bgOpacity}>This is a paragraph.</p>
<style>
p {
font-family: "Comic Sans MS", cursive;
background: rgba(255, 62, 0, var(--opacity));
}
</style>

@ -0,0 +1,18 @@
---
title: The style directive
---
Being able to set CSS properties dynamically is nice. However, this can get unwieldy if you have to write a long string. Mistakes like missing any of the semicolons could make the whole string invalid. Therefore, Svelte provides a nicer way to write inline styles with the style directive.
Change the style attribute of the paragraph to the following:
```html
<p
style:color
style:--opacity="{bgOpacity}"
>
```
The style directive shares a few qualities with the class directive. You can use a shorthand when the name of the property and the variable are the same. So `style:color="{color}"` can be written as just `style:color`.
Similar to the class directive, the style directive will take precedence if you try to set the same property through a style attribute.

@ -0,0 +1,3 @@
{
"title": "Advanced styling"
}

@ -1,3 +0,0 @@
{
"title": "Classes"
}

@ -242,6 +242,10 @@ export default {
code: 'invalid-animation', code: 'invalid-animation',
message: 'An element that uses the animate directive must be the immediate child of a keyed each block' message: 'An element that uses the animate directive must be the immediate child of a keyed each block'
}, },
invalid_animation_key: {
code: 'invalid-animation',
message: 'An element that uses the animate directive must be used inside a keyed each block. Did you forget to add a key to your each block?'
},
invalid_animation_sole: { invalid_animation_sole: {
code: 'invalid-animation', code: 'invalid-animation',
message: 'An element that uses the animate directive must be the sole child of a keyed each block' message: 'An element that uses the animate directive must be the sole child of a keyed each block'

@ -26,12 +26,17 @@ export default class Animation extends Node {
} }
const block = parent.parent; const block = parent.parent;
if (!block || block.type !== 'EachBlock' || !block.key) { if (!block || block.type !== 'EachBlock') {
// TODO can we relax the 'immediate child' rule? // TODO can we relax the 'immediate child' rule?
component.error(this, compiler_errors.invalid_animation_immediate); component.error(this, compiler_errors.invalid_animation_immediate);
return; return;
} }
if (!block.key) {
component.error(this, compiler_errors.invalid_animation_key);
return;
}
(block as EachBlock).has_animation = true; (block as EachBlock).has_animation = true;
this.expression = info.expression this.expression = info.expression

@ -63,7 +63,7 @@ export default class EachBlock extends AbstractBlock {
([this.const_tags, this.children] = get_const_tags(info.children, component, this, this)); ([this.const_tags, this.children] = get_const_tags(info.children, component, this, this));
if (this.has_animation) { if (this.has_animation) {
this.children = this.children.filter(child => !isEmptyNode(child)); this.children = this.children.filter(child => !isEmptyNode(child) && !isCommentNode(child));
if (this.children.length !== 1) { if (this.children.length !== 1) {
const child = this.children.find(child => !!(child as Element).animation); const child = this.children.find(child => !!(child as Element).animation);
@ -83,3 +83,6 @@ export default class EachBlock extends AbstractBlock {
function isEmptyNode(node: INode) { function isEmptyNode(node: INode) {
return node.type === 'Text' && node.data.trim() === ''; return node.type === 'Text' && node.data.trim() === '';
} }
function isCommentNode(node: INode) {
return node.type === 'Comment';
}

@ -27,10 +27,7 @@ export default class SlotTemplate extends Node {
this.validate_slot_template_placement(); this.validate_slot_template_placement();
const has_let = info.attributes.some((node) => node.type === 'Let');
if (has_let) {
scope = scope.child(); scope = scope.child();
}
info.attributes.forEach((node) => { info.attributes.forEach((node) => {
switch (node.type) { switch (node.type) {

@ -90,7 +90,7 @@ export default class Expression {
} }
if (template_scope.is_let(name)) { if (template_scope.is_let(name)) {
if (!function_expression) { // TODO should this be `!lazy` ? if (!lazy) {
contextual_dependencies.add(name); contextual_dependencies.add(name);
dependencies.add(name); dependencies.add(name);
} }

@ -2,7 +2,7 @@ import { custom_event, append, append_hydration, insert, insert_hydration, detac
import { SvelteComponent } from './Component'; import { SvelteComponent } from './Component';
export function dispatch_dev<T=any>(type: string, detail?: T) { export function dispatch_dev<T=any>(type: string, detail?: T) {
document.dispatchEvent(custom_event(type, { version: '__VERSION__', ...detail }, true)); document.dispatchEvent(custom_event(type, { version: '__VERSION__', ...detail }, { bubbles: true }));
} }
export function append_dev(target: Node, node: Node) { export function append_dev(target: Node, node: Node) {

@ -634,9 +634,9 @@ export function toggle_class(element, name, toggle) {
element.classList[toggle ? 'add' : 'remove'](name); element.classList[toggle ? 'add' : 'remove'](name);
} }
export function custom_event<T=any>(type: string, detail?: T, bubbles: boolean = false) { export function custom_event<T=any>(type: string, detail?: T, { bubbles = false, cancelable = false } = {}): CustomEvent<T> {
const e: CustomEvent<T> = document.createEvent('CustomEvent'); const e: CustomEvent<T> = document.createEvent('CustomEvent');
e.initCustomEvent(type, bubbles, false, detail); e.initCustomEvent(type, bubbles, cancelable, detail);
return e; return e;
} }

@ -27,22 +27,33 @@ export function onDestroy(fn: () => any) {
get_current_component().$$.on_destroy.push(fn); get_current_component().$$.on_destroy.push(fn);
} }
export function createEventDispatcher< export interface DispatchOptions {
EventMap extends {} = any cancelable?: boolean;
>(): <EventKey extends Extract<keyof EventMap, string>>(type: EventKey, detail?: EventMap[EventKey]) => void { }
export function createEventDispatcher<EventMap extends {} = any>(): <
EventKey extends Extract<keyof EventMap, string>
>(
type: EventKey,
detail?: EventMap[EventKey],
options?: DispatchOptions
) => boolean {
const component = get_current_component(); const component = get_current_component();
return (type: string, detail?: any) => { return (type: string, detail?: any, { cancelable = false } = {}): boolean => {
const callbacks = component.$$.callbacks[type]; const callbacks = component.$$.callbacks[type];
if (callbacks) { if (callbacks) {
// TODO are there situations where events could be dispatched // TODO are there situations where events could be dispatched
// in a server (non-DOM) environment? // in a server (non-DOM) environment?
const event = custom_event(type, detail); const event = custom_event(type, detail, { cancelable });
callbacks.slice().forEach(fn => { callbacks.slice().forEach(fn => {
fn.call(component, event); fn.call(component, event);
}); });
return !event.defaultPrevented;
} }
return true;
}; };
} }

@ -0,0 +1,5 @@
<script>
export let prop
</script>
<slot value={prop} />

@ -0,0 +1,25 @@
let logs;
function log(value) {
logs.push(value);
}
export default {
props: {
prop: 'a',
log
},
html: '<button></button>',
before_test() {
logs = [];
},
async test({ assert, component, target, window }) {
const button = target.querySelector('button');
await button.dispatchEvent(new window.MouseEvent('click'));
assert.deepEqual(logs, ['a']);
component.prop = 'b';
await button.dispatchEvent(new window.MouseEvent('click'));
assert.deepEqual(logs, ['a', 'b']);
}
};

@ -0,0 +1,11 @@
<script>
import Outer from './Outer.svelte'
import Inner from './Inner.svelte'
export let prop
export let log;
</script>
<Outer {prop} let:value>
<Inner><button on:click={() => { log(value); }} /></Inner>
</Outer>

@ -0,0 +1,15 @@
export default {
html: `
<div>static dynamic</div>
<div>static dynamic</div>
<div>static dynamic</div>
`,
async test({ component, target, assert }) {
component.props = 'xxx';
assert.htmlEqual(target.innerHTML, `
<div>static xxx</div>
<div>static xxx</div>
<div>static xxx</div>
`);
}
};

@ -0,0 +1,24 @@
<script>
import Component from './Component.svelte';
export let props = "dynamic";
</script>
<Component>
<svelte:fragment slot="box1">
{@const foo = "static"}
{@const bar = props}
<div>{foo} {bar}</div>
</svelte:fragment>
<svelte:fragment>
{@const foo = "static"}
{@const bar = props}
<div>{foo} {bar}</div>
</svelte:fragment>
</Component>
<Component>
{@const foo = "static"}
{@const bar = props}
<div>{foo} {bar}</div>
</Component>

@ -0,0 +1,9 @@
<script>
export let things;
function foo() {}
</script>
{#each things as thing (thing)}
<!-- some comment -->
<div animate:foo></div>
{/each}

@ -1,6 +1,6 @@
[{ [{
"code": "invalid-animation", "code": "invalid-animation",
"message": "An element that uses the animate directive must be the immediate child of a keyed each block", "message": "An element that uses the animate directive must be used inside a keyed each block. Did you forget to add a key to your each block?",
"start": { "start": {
"line": 6, "line": 6,
"column": 6, "column": 6,

Loading…
Cancel
Save