diff --git a/CHANGELOG.md b/CHANGELOG.md index 41d70eedcc..ccdf043a72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Also: * Flush changes in newly attached block when using `{#await}` ([#3660](https://github.com/sveltejs/svelte/issues/3660)) * Throw exception immediately when calling `createEventDispatcher()` after component instantiation ([#3667](https://github.com/sveltejs/svelte/pull/3667)) * Fix globals shadowing contextual template scope ([#3674](https://github.com/sveltejs/svelte/issues/3674)) +* Fix `` bindings to stores ([#3832](https://github.com/sveltejs/svelte/issues/3832)) ## 3.12.1 diff --git a/package.json b/package.json index c60ceb2597..27f330f9fc 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@types/node": "^8.10.53", "@typescript-eslint/eslint-plugin": "^1.13.0", "@typescript-eslint/parser": "^2.1.0", + "@rollup/plugin-replace": "^2.2.1", "acorn": "^7.1.0", "agadoo": "^1.1.0", "c8": "^5.0.1", @@ -82,7 +83,6 @@ "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-json": "^4.0.0", "rollup-plugin-node-resolve": "^5.2.0", - "rollup-plugin-replace": "^2.2.0", "rollup-plugin-sucrase": "^2.1.0", "rollup-plugin-typescript": "^1.0.1", "rollup-plugin-virtual": "^1.0.1", diff --git a/rollup.config.js b/rollup.config.js index 4444494a5f..c55cbf2426 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,5 +1,5 @@ import fs from 'fs'; -import replace from 'rollup-plugin-replace'; +import replace from '@rollup/plugin-replace'; import resolve from 'rollup-plugin-node-resolve'; import commonjs from 'rollup-plugin-commonjs'; import json from 'rollup-plugin-json'; diff --git a/site/content/examples/05-bindings/10-bind-this/App.svelte b/site/content/examples/05-bindings/10-bind-this/App.svelte index 9a8fabc265..8e4b3c5bef 100644 --- a/site/content/examples/05-bindings/10-bind-this/App.svelte +++ b/site/content/examples/05-bindings/10-bind-this/App.svelte @@ -43,8 +43,8 @@ width: 100%; height: 100%; background-color: #666; - -webkit-mask: url(logo-mask.svg) 50% 50% no-repeat; - mask: url(logo-mask.svg) 50% 50% no-repeat; + -webkit-mask: url(svelte-logo-mask.svg) 50% 50% no-repeat; + mask: url(svelte-logo-mask.svg) 50% 50% no-repeat; } diff --git a/site/content/tutorial/06-bindings/12-bind-this/app-a/App.svelte b/site/content/tutorial/06-bindings/12-bind-this/app-a/App.svelte index c85cab9225..d40c45ef6c 100644 --- a/site/content/tutorial/06-bindings/12-bind-this/app-a/App.svelte +++ b/site/content/tutorial/06-bindings/12-bind-this/app-a/App.svelte @@ -43,8 +43,8 @@ width: 100%; height: 100%; background-color: #666; - -webkit-mask: url(logo-mask.svg) 50% 50% no-repeat; - mask: url(logo-mask.svg) 50% 50% no-repeat; + -webkit-mask: url(svelte-logo-mask.svg) 50% 50% no-repeat; + mask: url(svelte-logo-mask.svg) 50% 50% no-repeat; } diff --git a/site/content/tutorial/06-bindings/12-bind-this/app-b/App.svelte b/site/content/tutorial/06-bindings/12-bind-this/app-b/App.svelte index 9a8fabc265..8e4b3c5bef 100644 --- a/site/content/tutorial/06-bindings/12-bind-this/app-b/App.svelte +++ b/site/content/tutorial/06-bindings/12-bind-this/app-b/App.svelte @@ -43,8 +43,8 @@ width: 100%; height: 100%; background-color: #666; - -webkit-mask: url(logo-mask.svg) 50% 50% no-repeat; - mask: url(logo-mask.svg) 50% 50% no-repeat; + -webkit-mask: url(svelte-logo-mask.svg) 50% 50% no-repeat; + mask: url(svelte-logo-mask.svg) 50% 50% no-repeat; } diff --git a/site/content/tutorial/16-special-elements/07-svelte-options/app-a/Todo.svelte b/site/content/tutorial/16-special-elements/07-svelte-options/app-a/Todo.svelte index dae595e7d0..5e0dbca300 100644 --- a/site/content/tutorial/16-special-elements/07-svelte-options/app-a/Todo.svelte +++ b/site/content/tutorial/16-special-elements/07-svelte-options/app-a/Todo.svelte @@ -3,7 +3,6 @@ import flash from './flash.js'; export let todo; - export let toggle; let div; diff --git a/site/content/tutorial/16-special-elements/07-svelte-options/app-b/Todo.svelte b/site/content/tutorial/16-special-elements/07-svelte-options/app-b/Todo.svelte index 447ddc601c..e7ddcedc47 100644 --- a/site/content/tutorial/16-special-elements/07-svelte-options/app-b/Todo.svelte +++ b/site/content/tutorial/16-special-elements/07-svelte-options/app-b/Todo.svelte @@ -5,7 +5,6 @@ import flash from './flash.js'; export let todo; - export let toggle; let div; diff --git a/site/static/logo-mask.svg b/site/static/logo-mask.svg deleted file mode 100644 index d7919a61ab..0000000000 --- a/site/static/logo-mask.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file diff --git a/src/compiler/compile/css/Selector.ts b/src/compiler/compile/css/Selector.ts index ab19ebd1e1..d99af7a110 100644 --- a/src/compiler/compile/css/Selector.ts +++ b/src/compiler/compile/css/Selector.ts @@ -3,6 +3,7 @@ import Stylesheet from './Stylesheet'; import { gather_possible_values, UNKNOWN } from './gather_possible_values'; import { CssNode } from './interfaces'; import Component from '../Component'; +import Element from '../nodes/Element'; enum BlockAppliesToNode { NotPossible, @@ -34,8 +35,8 @@ export default class Selector { this.used = this.blocks[0].global; } - apply(node: CssNode, stack: CssNode[]) { - const to_encapsulate: CssNode[] = []; + apply(node: Element, stack: Element[]) { + const to_encapsulate: any[] = []; apply_selector(this.local_blocks.slice(), node, stack.slice(), to_encapsulate); @@ -132,7 +133,7 @@ export default class Selector { } } -function apply_selector(blocks: Block[], node: CssNode, stack: CssNode[], to_encapsulate: any[]): boolean { +function apply_selector(blocks: Block[], node: Element, stack: Element[], to_encapsulate: any[]): boolean { const block = blocks.pop(); if (!block) return false; @@ -259,16 +260,84 @@ function attribute_matches(node: CssNode, name: string, expected_value: string, const attr = node.attributes.find((attr: CssNode) => attr.name === name); if (!attr) return false; if (attr.is_true) return operator === null; - if (attr.chunks.length > 1) return true; if (!expected_value) return true; - const value = attr.chunks[0]; - - if (!value) return false; - if (value.type === 'Text') return test_attribute(operator, expected_value, case_insensitive, value.data); + if (attr.chunks.length === 1) { + const value = attr.chunks[0]; + if (!value) return false; + if (value.type === 'Text') return test_attribute(operator, expected_value, case_insensitive, value.data); + } const possible_values = new Set(); - gather_possible_values(value.node, possible_values); + + let prev_values = []; + for (const chunk of attr.chunks) { + const current_possible_values = new Set(); + if (chunk.type === 'Text') { + current_possible_values.add(chunk.data); + } else { + gather_possible_values(chunk.node, current_possible_values); + } + + // impossible to find out all combinations + if (current_possible_values.has(UNKNOWN)) return true; + + if (prev_values.length > 0) { + const start_with_space = []; + const remaining = []; + current_possible_values.forEach((current_possible_value: string) => { + if (/^\s/.test(current_possible_value)) { + start_with_space.push(current_possible_value); + } else { + remaining.push(current_possible_value); + } + }); + + if (remaining.length > 0) { + if (start_with_space.length > 0) { + prev_values.forEach(prev_value => possible_values.add(prev_value)); + } + + const combined = []; + prev_values.forEach((prev_value: string) => { + remaining.forEach((value: string) => { + combined.push(prev_value + value); + }); + }); + prev_values = combined; + + start_with_space.forEach((value: string) => { + if (/\s$/.test(value)) { + possible_values.add(value); + } else { + prev_values.push(value); + } + }); + continue; + } else { + prev_values.forEach(prev_value => possible_values.add(prev_value)); + prev_values = []; + } + } + + current_possible_values.forEach((current_possible_value: string) => { + if (/\s$/.test(current_possible_value)) { + possible_values.add(current_possible_value); + } else { + prev_values.push(current_possible_value); + } + }); + if (prev_values.length < current_possible_values.size) { + prev_values.push(' '); + } + + if (prev_values.length > 20) { + // might grow exponentially, bail out + return true; + } + } + prev_values.forEach(prev_value => possible_values.add(prev_value)); + if (possible_values.has(UNKNOWN)) return true; for (const value of possible_values) { diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index d353d20158..555c772f23 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -151,6 +151,10 @@ export default class Element extends Node { } } + // Binding relies on Attribute, defer its evaluation + const order = ['Binding']; // everything else is -1 + info.attributes.sort((a, b) => order.indexOf(a.type) - order.indexOf(b.type)); + info.attributes.forEach(node => { switch (node.type) { case 'Action': diff --git a/src/compiler/compile/nodes/EventHandler.ts b/src/compiler/compile/nodes/EventHandler.ts index 7faf5d15dd..19da3d9dd7 100644 --- a/src/compiler/compile/nodes/EventHandler.ts +++ b/src/compiler/compile/nodes/EventHandler.ts @@ -1,8 +1,6 @@ import Node from './shared/Node'; import Expression from './shared/Expression'; import Component from '../Component'; -import { b, x } from 'code-red'; -import Block from '../render_dom/Block'; import { sanitize } from '../../utils/names'; import { Identifier } from 'estree'; @@ -14,6 +12,7 @@ export default class EventHandler extends Node { handler_name: Identifier; uses_context = false; can_make_passive = false; + reassigned?: boolean; constructor(component: Component, parent, template_scope, info) { super(component, parent, template_scope, info); @@ -22,7 +21,7 @@ export default class EventHandler extends Node { this.modifiers = new Set(info.modifiers); if (info.expression) { - this.expression = new Expression(component, this, template_scope, info.expression, true); + this.expression = new Expression(component, this, template_scope, info.expression); this.uses_context = this.expression.uses_context; if (/FunctionExpression/.test(info.expression.type) && info.expression.params.length === 0) { @@ -42,34 +41,12 @@ export default class EventHandler extends Node { if (node && (node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration' || node.type === 'ArrowFunctionExpression') && node.params.length === 0) { this.can_make_passive = true; } + + this.reassigned = component.var_lookup.get(info.expression.name).reassigned; } } } else { - const id = component.get_unique_name(`${sanitize(this.name)}_handler`); - - component.add_var({ - name: id.name, - internal: true, - referenced: true - }); - - component.partly_hoisted.push(b` - function ${id}(event) { - @bubble($$self, event); - } - `); - - this.handler_name = id; + this.handler_name = component.get_unique_name(`${sanitize(this.name)}_handler`); } } - - // TODO move this? it is specific to render-dom - render(block: Block) { - if (this.expression) { - return this.expression.manipulate(block); - } - - // this.component.add_reference(this.handler_name); - return x`#ctx.${this.handler_name}`; - } } diff --git a/src/compiler/compile/render_dom/Block.ts b/src/compiler/compile/render_dom/Block.ts index b268954dd0..56b5d08489 100644 --- a/src/compiler/compile/render_dom/Block.ts +++ b/src/compiler/compile/render_dom/Block.ts @@ -203,13 +203,11 @@ export default class Block { } add_variable(id: Identifier, init?: Node) { - this.variables.forEach(v => { - if (v.id.name === id.name) { - throw new Error( - `Variable '${id.name}' already initialised with a different value` - ); - } - }); + if (this.variables.has(id.name)) { + throw new Error( + `Variable '${id.name}' already initialised with a different value` + ); + } this.variables.set(id.name, { id, init }); } @@ -376,6 +374,8 @@ export default class Block { d: ${properties.destroy} }`; + const block = dev && this.get_unique_name('block'); + const body = b` ${Array.from(this.variables.values()).map(({ id, init }) => { return init @@ -387,9 +387,15 @@ export default class Block { ${dev ? b` - const block = ${return_value}; - @dispatch_dev("SvelteRegisterBlock", { block, id: ${this.name || 'create_fragment'}.name, type: "${this.type}", source: "${this.comment ? this.comment.replace(/"/g, '\\"') : ''}", ctx: #ctx }); - return block;` + const ${block} = ${return_value}; + @dispatch_dev("SvelteRegisterBlock", { + block: ${block}, + id: ${this.name || 'create_fragment'}.name, + type: "${this.type}", + source: "${this.comment ? this.comment.replace(/"/g, '\\"') : ''}", + ctx: #ctx + }); + return ${block};` : b` return ${return_value};` } @@ -398,6 +404,21 @@ export default class Block { return body; } + has_content() { + return this.renderer.options.dev || + this.first || + this.event_listeners.length > 0 || + this.chunks.intro.length > 0 || + this.chunks.outro.length > 0 || + this.chunks.create.length > 0 || + this.chunks.hydrate.length > 0 || + this.chunks.claim.length > 0 || + this.chunks.mount.length > 0 || + this.chunks.update.length > 0 || + this.chunks.destroy.length > 0 || + this.has_animation; + } + render() { const key = this.key && this.get_unique_name('key'); diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts index a50a86e2f3..49ce17236a 100644 --- a/src/compiler/compile/render_dom/index.ts +++ b/src/compiler/compile/render_dom/index.ts @@ -241,11 +241,16 @@ export default function dom( args.push(x`$$props`, x`$$invalidate`); } - body.push(b` - function create_fragment(#ctx) { - ${block.get_contents()} - } + const has_create_fragment = block.has_content(); + if (has_create_fragment) { + body.push(b` + function create_fragment(#ctx) { + ${block.get_contents()} + } + `); + } + body.push(b` ${component.extract_javascript(component.ast.module)} ${component.fully_hoisted} @@ -437,7 +442,7 @@ export default function dom( ${css.code && b`this.shadowRoot.innerHTML = \`\`;`} - @init(this, { target: this.shadowRoot }, ${definition}, create_fragment, ${not_equal}, ${prop_names}); + @init(this, { target: this.shadowRoot }, ${definition}, ${has_create_fragment ? 'create_fragment': 'null'}, ${not_equal}, ${prop_names}); ${dev_props_check} @@ -489,7 +494,7 @@ export default function dom( constructor(options) { super(${options.dev && `options`}); ${should_add_css && b`if (!@_document.getElementById("${component.stylesheet.id}-style")) ${add_css}();`} - @init(this, options, ${definition}, create_fragment, ${not_equal}, ${prop_names}); + @init(this, options, ${definition}, ${has_create_fragment ? 'create_fragment': 'null'}, ${not_equal}, ${prop_names}); ${options.dev && b`@dispatch_dev("SvelteRegisterComponent", { component: this, tagName: "${name.name}", options, id: create_fragment.name });`} ${dev_props_check} diff --git a/src/compiler/compile/render_dom/wrappers/Body.ts b/src/compiler/compile/render_dom/wrappers/Body.ts index dc1f561b1f..e16ebc25bd 100644 --- a/src/compiler/compile/render_dom/wrappers/Body.ts +++ b/src/compiler/compile/render_dom/wrappers/Body.ts @@ -3,21 +3,24 @@ import Wrapper from './shared/Wrapper'; import { b } from 'code-red'; import Body from '../../nodes/Body'; import { Identifier } from 'estree'; +import EventHandler from './Element/EventHandler'; export default class BodyWrapper extends Wrapper { node: Body; render(block: Block, _parent_node: Identifier, _parent_nodes: Identifier) { - this.node.handlers.forEach(handler => { - const snippet = handler.render(block); + this.node.handlers + .map(handler => new EventHandler(handler, this)) + .forEach(handler => { + const snippet = handler.get_snippet(block); - block.chunks.init.push(b` - @_document.body.addEventListener("${handler.name}", ${snippet}); - `); + block.chunks.init.push(b` + @_document.body.addEventListener("${handler.node.name}", ${snippet}); + `); - block.chunks.destroy.push(b` - @_document.body.removeEventListener("${handler.name}", ${snippet}); - `); - }); + block.chunks.destroy.push(b` + @_document.body.removeEventListener("${handler.node.name}", ${snippet}); + `); + }); } } diff --git a/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts b/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts new file mode 100644 index 0000000000..37089e7493 --- /dev/null +++ b/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts @@ -0,0 +1,69 @@ +import EventHandler from '../../../nodes/EventHandler'; +import Wrapper from '../shared/Wrapper'; +import Block from '../../Block'; +import { b, x, p } from 'code-red'; + +const TRUE = x`true`; +const FALSE = x`false`; + +export default class EventHandlerWrapper { + node: EventHandler; + parent: Wrapper; + + constructor(node: EventHandler, parent: Wrapper) { + this.node = node; + this.parent = parent; + + if (!node.expression) { + this.parent.renderer.component.add_var({ + name: node.handler_name.name, + internal: true, + referenced: true, + }); + + this.parent.renderer.component.partly_hoisted.push(b` + function ${node.handler_name.name}(event) { + @bubble($$self, event); + } + `); + } + } + + get_snippet(block) { + const snippet = this.node.expression ? this.node.expression.manipulate(block) : x`#ctx.${this.node.handler_name}`; + + if (this.node.reassigned) { + block.maintain_context = true; + return x`function () { ${snippet}.apply(this, arguments); }`; + } + return snippet; + } + + render(block: Block, target: string) { + let snippet = this.get_snippet(block); + + if (this.node.modifiers.has('preventDefault')) snippet = x`@prevent_default(${snippet})`; + if (this.node.modifiers.has('stopPropagation')) snippet = x`@stop_propagation(${snippet})`; + if (this.node.modifiers.has('self')) snippet = x`@self(${snippet})`; + + const args = []; + + const opts = ['passive', 'once', 'capture'].filter(mod => this.node.modifiers.has(mod)); + if (opts.length) { + args.push((opts.length === 1 && opts[0] === 'capture') + ? TRUE + : x`{ ${opts.map(opt => p`${opt}: true`)} }`); + } else if (block.renderer.options.dev) { + args.push(FALSE); + } + + if (block.renderer.options.dev) { + args.push(this.node.modifiers.has('stopPropagation') ? TRUE : FALSE); + args.push(this.node.modifiers.has('preventDefault') ? TRUE : FALSE); + } + + block.event_listeners.push( + x`@listen(${target}, "${this.node.name}", ${snippet}, ${args})` + ); + } +} diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index a80db84169..9b7bbfef74 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -24,6 +24,7 @@ import bind_this from '../shared/bind_this'; import { changed } from '../shared/changed'; import { is_head } from '../shared/is_head'; import { Identifier } from 'estree'; +import EventHandler from './EventHandler'; const events = [ { @@ -113,6 +114,7 @@ export default class ElementWrapper extends Wrapper { fragment: FragmentWrapper; attributes: AttributeWrapper[]; bindings: Binding[]; + event_handlers: EventHandler[]; class_dependencies: string[]; slot_block: Block; @@ -194,6 +196,8 @@ export default class ElementWrapper extends Wrapper { // e.g.