From ba5ede599a9e7b3991c235a8ae0d9c5efefeaa3e Mon Sep 17 00:00:00 2001 From: Jacob Wright Date: Thu, 23 Aug 2018 17:23:55 -0600 Subject: [PATCH 01/15] Allows actions to use any expression type Allow any expression to pass data to an action. Added a test for a ternary statement and a string template. Fixes #1676 --- src/parse/read/directives.ts | 5 +++-- .../action-ternary-template/_config.js | 20 ++++++++++++++++++ .../samples/action-ternary-template/main.html | 21 +++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 test/runtime/samples/action-ternary-template/_config.js create mode 100644 test/runtime/samples/action-ternary-template/main.html diff --git a/src/parse/read/directives.ts b/src/parse/read/directives.ts index 62a249a158..a1d71a7ef6 100644 --- a/src/parse/read/directives.ts +++ b/src/parse/read/directives.ts @@ -77,7 +77,7 @@ const DIRECTIVES: Record + `, + + test ( assert, component, target, window ) { + const header = target.querySelector( 'h1' ); + const eventClick = new window.MouseEvent( 'click' ); + + header.dispatchEvent( eventClick ); + assert.htmlEqual( target.innerHTML, ` +

Hello World!

+ ` ); + } +}; diff --git a/test/runtime/samples/action-ternary-template/main.html b/test/runtime/samples/action-ternary-template/main.html new file mode 100644 index 0000000000..58e19e28f4 --- /dev/null +++ b/test/runtime/samples/action-ternary-template/main.html @@ -0,0 +1,21 @@ +

+ + \ No newline at end of file From af1902d510282018cce9f27f804a211739b0a8e7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Aug 2018 20:58:49 -0400 Subject: [PATCH 02/15] run transitions in context of component - fixes #1675 --- src/shared/index.js | 4 ---- src/shared/transitions.js | 8 +++----- src/shared/utils.js | 4 ++++ .../samples/transition-js-context/_config.js | 9 +++++++++ .../samples/transition-js-context/main.html | 20 +++++++++++++++++++ 5 files changed, 36 insertions(+), 9 deletions(-) create mode 100644 test/runtime/samples/transition-js-context/_config.js create mode 100644 test/runtime/samples/transition-js-context/main.html diff --git a/src/shared/index.js b/src/shared/index.js index 47e7a75099..898e816beb 100644 --- a/src/shared/index.js +++ b/src/shared/index.js @@ -97,10 +97,6 @@ export function on(eventName, handler) { }; } -export function run(fn) { - fn(); -} - export function set(newState) { this._set(assign({}, newState)); if (this.root._lock) return; diff --git a/src/shared/transitions.js b/src/shared/transitions.js index 1aa430a3b3..8dfa50b8ff 100644 --- a/src/shared/transitions.js +++ b/src/shared/transitions.js @@ -1,5 +1,5 @@ import { createElement } from './dom.js'; -import { noop } from './utils.js'; +import { noop, run } from './utils.js'; export function linear(t) { return t; @@ -27,7 +27,7 @@ export function hash(str) { } export function wrapTransition(component, node, fn, params, intro) { - let obj = fn(node, params); + let obj = fn.call(component, node, params); let duration; let ease; let cssText; @@ -137,9 +137,7 @@ export function wrapTransition(component, node, fn, params, intro) { }); if (--program.group.remaining === 0) { - program.group.callbacks.forEach(fn => { - fn(); - }); + program.group.callbacks.forEach(run); } } else { if (obj.css) transitionManager.deleteRule(node, program.name); diff --git a/src/shared/utils.js b/src/shared/utils.js index 1394f55a46..2077c25e01 100644 --- a/src/shared/utils.js +++ b/src/shared/utils.js @@ -31,4 +31,8 @@ export function exclude(src, prop) { const tar = {}; for (const k in src) k === prop || (tar[k] = src[k]); return tar; +} + +export function run(fn) { + fn(); } \ No newline at end of file diff --git a/test/runtime/samples/transition-js-context/_config.js b/test/runtime/samples/transition-js-context/_config.js new file mode 100644 index 0000000000..c5b3f1460a --- /dev/null +++ b/test/runtime/samples/transition-js-context/_config.js @@ -0,0 +1,9 @@ +export default { + test(assert, component, target, window, raf) { + const div = target.querySelector('div'); + assert.equal(div.foo, 42); + + raf.tick(50); + assert.equal(div.foo, 42); + } +}; diff --git a/test/runtime/samples/transition-js-context/main.html b/test/runtime/samples/transition-js-context/main.html new file mode 100644 index 0000000000..18f5f50ec9 --- /dev/null +++ b/test/runtime/samples/transition-js-context/main.html @@ -0,0 +1,20 @@ +
+ + \ No newline at end of file From ec6360487ef4c419d9cd12bf749c6e32a0d1b186 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Aug 2018 22:02:16 -0400 Subject: [PATCH 03/15] correctly set select value on mount - fixes #1666 --- src/compile/nodes/Attribute.ts | 2 +- .../select-dynamic-value/expected-bundle.js | 224 ++++++++++++++++++ .../samples/select-dynamic-value/expected.js | 73 ++++++ .../samples/select-dynamic-value/input.html | 4 + 4 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 test/js/samples/select-dynamic-value/expected-bundle.js create mode 100644 test/js/samples/select-dynamic-value/expected.js create mode 100644 test/js/samples/select-dynamic-value/input.html diff --git a/src/compile/nodes/Attribute.ts b/src/compile/nodes/Attribute.ts index 0165dc98fb..b0c26c534d 100644 --- a/src/compile/nodes/Attribute.ts +++ b/src/compile/nodes/Attribute.ts @@ -210,7 +210,7 @@ export default class Attribute extends Node { } `; - block.builders.hydrate.addBlock(deindent` + block.builders.mount.addBlock(deindent` ${last} = ${value}; ${updater} `); diff --git a/test/js/samples/select-dynamic-value/expected-bundle.js b/test/js/samples/select-dynamic-value/expected-bundle.js new file mode 100644 index 0000000000..1230856164 --- /dev/null +++ b/test/js/samples/select-dynamic-value/expected-bundle.js @@ -0,0 +1,224 @@ +function noop() {} + +function assign(tar, src) { + for (var k in src) tar[k] = src[k]; + return tar; +} + +function append(target, node) { + target.appendChild(node); +} + +function insert(target, node, anchor) { + target.insertBefore(node, anchor); +} + +function detachNode(node) { + node.parentNode.removeChild(node); +} + +function createElement(name) { + return document.createElement(name); +} + +function createText(data) { + return document.createTextNode(data); +} + +function blankObject() { + return Object.create(null); +} + +function destroy(detach) { + this.destroy = noop; + this.fire('destroy'); + this.set = noop; + + this._fragment.d(detach !== false); + this._fragment = null; + this._state = {}; +} + +function _differs(a, b) { + return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function'); +} + +function fire(eventName, data) { + var handlers = + eventName in this._handlers && this._handlers[eventName].slice(); + if (!handlers) return; + + for (var i = 0; i < handlers.length; i += 1) { + var handler = handlers[i]; + + if (!handler.__calling) { + try { + handler.__calling = true; + handler.call(this, data); + } finally { + handler.__calling = false; + } + } + } +} + +function flush(component) { + component._lock = true; + callAll(component._beforecreate); + callAll(component._oncreate); + callAll(component._aftercreate); + component._lock = false; +} + +function get() { + return this._state; +} + +function init(component, options) { + component._handlers = blankObject(); + component._slots = blankObject(); + component._bind = options._bind; + + component.options = options; + component.root = options.root || component; + component.store = options.store || component.root.store; + + if (!options.root) { + component._beforecreate = []; + component._oncreate = []; + component._aftercreate = []; + } +} + +function on(eventName, handler) { + var handlers = this._handlers[eventName] || (this._handlers[eventName] = []); + handlers.push(handler); + + return { + cancel: function() { + var index = handlers.indexOf(handler); + if (~index) handlers.splice(index, 1); + } + }; +} + +function set(newState) { + this._set(assign({}, newState)); + if (this.root._lock) return; + flush(this.root); +} + +function _set(newState) { + var oldState = this._state, + changed = {}, + dirty = false; + + for (var key in newState) { + if (this._differs(newState[key], oldState[key])) changed[key] = dirty = true; + } + if (!dirty) return; + + this._state = assign(assign({}, oldState), newState); + this._recompute(changed, this._state); + if (this._bind) this._bind(changed, this._state); + + if (this._fragment) { + this.fire("state", { changed: changed, current: this._state, previous: oldState }); + this._fragment.p(changed, this._state); + this.fire("update", { changed: changed, current: this._state, previous: oldState }); + } +} + +function callAll(fns) { + while (fns && fns.length) fns.shift()(); +} + +function _mount(target, anchor) { + this._fragment[this._fragment.i ? 'i' : 'm'](target, anchor || null); +} + +var proto = { + destroy, + get, + fire, + on, + set, + _recompute: noop, + _set, + _mount, + _differs +}; + +/* generated by Svelte vX.Y.Z */ + +function create_main_fragment(component, ctx) { + var select, option, text, option_1, text_1, select_value_value; + + return { + c() { + select = createElement("select"); + option = createElement("option"); + text = createText("1"); + option_1 = createElement("option"); + text_1 = createText("2"); + option.__value = "1"; + option.value = option.__value; + option_1.__value = "2"; + option_1.value = option_1.__value; + }, + + m(target, anchor) { + insert(target, select, anchor); + append(select, option); + append(option, text); + append(select, option_1); + append(option_1, text_1); + + select_value_value = ctx.current; + for (var i = 0; i < select.options.length; i += 1) { + var option_2 = select.options[i]; + + if (option_2.__value === select_value_value) { + option_2.selected = true; + break; + } + } + }, + + p(changed, ctx) { + if ((changed.current) && select_value_value !== (select_value_value = ctx.current)) { + for (var i = 0; i < select.options.length; i += 1) { + var option_2 = select.options[i]; + + if (option_2.__value === select_value_value) { + option_2.selected = true; + break; + } + } + } + }, + + d(detach) { + if (detach) { + detachNode(select); + } + } + }; +} + +function SvelteComponent(options) { + init(this, options); + this._state = assign({}, options.data); + this._intro = true; + + this._fragment = create_main_fragment(this, this._state); + + if (options.target) { + this._fragment.c(); + this._mount(options.target, options.anchor); + } +} + +assign(SvelteComponent.prototype, proto); + +export default SvelteComponent; diff --git a/test/js/samples/select-dynamic-value/expected.js b/test/js/samples/select-dynamic-value/expected.js new file mode 100644 index 0000000000..9bf99b528d --- /dev/null +++ b/test/js/samples/select-dynamic-value/expected.js @@ -0,0 +1,73 @@ +/* generated by Svelte vX.Y.Z */ +import { append, assign, createElement, createText, detachNode, init, insert, proto } from "svelte/shared.js"; + +function create_main_fragment(component, ctx) { + var select, option, text, option_1, text_1, select_value_value; + + return { + c() { + select = createElement("select"); + option = createElement("option"); + text = createText("1"); + option_1 = createElement("option"); + text_1 = createText("2"); + option.__value = "1"; + option.value = option.__value; + option_1.__value = "2"; + option_1.value = option_1.__value; + }, + + m(target, anchor) { + insert(target, select, anchor); + append(select, option); + append(option, text); + append(select, option_1); + append(option_1, text_1); + + select_value_value = ctx.current; + for (var i = 0; i < select.options.length; i += 1) { + var option_2 = select.options[i]; + + if (option_2.__value === select_value_value) { + option_2.selected = true; + break; + } + } + }, + + p(changed, ctx) { + if ((changed.current) && select_value_value !== (select_value_value = ctx.current)) { + for (var i = 0; i < select.options.length; i += 1) { + var option_2 = select.options[i]; + + if (option_2.__value === select_value_value) { + option_2.selected = true; + break; + } + } + } + }, + + d(detach) { + if (detach) { + detachNode(select); + } + } + }; +} + +function SvelteComponent(options) { + init(this, options); + this._state = assign({}, options.data); + this._intro = true; + + this._fragment = create_main_fragment(this, this._state); + + if (options.target) { + this._fragment.c(); + this._mount(options.target, options.anchor); + } +} + +assign(SvelteComponent.prototype, proto); +export default SvelteComponent; \ No newline at end of file diff --git a/test/js/samples/select-dynamic-value/input.html b/test/js/samples/select-dynamic-value/input.html new file mode 100644 index 0000000000..2c7d174d92 --- /dev/null +++ b/test/js/samples/select-dynamic-value/input.html @@ -0,0 +1,4 @@ + \ No newline at end of file From 154ee7376509d49a30cc108cfa8acad232676482 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Aug 2018 22:53:17 -0400 Subject: [PATCH 04/15] support debug tag in SSR mode - fixes #1659 --- src/compile/nodes/DebugTag.ts | 21 +++++++- src/shared/ssr.js | 6 +++ test/js/samples/debug-ssr-foo/_config.js | 6 +++ .../samples/debug-ssr-foo/expected-bundle.js | 46 +++++++++++++++++ test/js/samples/debug-ssr-foo/expected.js | 51 +++++++++++++++++++ test/js/samples/debug-ssr-foo/input.html | 6 +++ 6 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 test/js/samples/debug-ssr-foo/_config.js create mode 100644 test/js/samples/debug-ssr-foo/expected-bundle.js create mode 100644 test/js/samples/debug-ssr-foo/expected.js create mode 100644 test/js/samples/debug-ssr-foo/input.html diff --git a/src/compile/nodes/DebugTag.ts b/src/compile/nodes/DebugTag.ts index e70d6a33a2..b73208961a 100644 --- a/src/compile/nodes/DebugTag.ts +++ b/src/compile/nodes/DebugTag.ts @@ -4,6 +4,7 @@ import Block from '../dom/Block'; import Expression from './shared/Expression'; import deindent from '../../utils/deindent'; import addToSet from '../../utils/addToSet'; +import { stringify } from '../../utils/stringify'; export default class DebugTag extends Node { expressions: Expression[]; @@ -25,8 +26,8 @@ export default class DebugTag extends Node { const { code } = this.compiler; - // Debug all if (this.expressions.length === 0) { + // Debug all code.overwrite(this.start + 1, this.start + 7, 'debugger', { storeName: true }); @@ -67,4 +68,22 @@ export default class DebugTag extends Node { `); } } + + ssr() { + if (!this.compiler.options.dev) return; + + const filename = this.compiler.file || null; + const { line, column } = this.compiler.locate(this.start + 1); + + const obj = this.expressions.length === 0 + ? `ctx` + : `{ ${this.expressions + .map(e => e.node.name) + .map(name => `${name}: ctx.${name}`) + .join(', ')} }`; + + const str = '${@debug(' + `${filename && stringify(filename)}, ${line}, ${column}, ${obj})}`; + + this.compiler.target.append(str); + } } \ No newline at end of file diff --git a/src/shared/ssr.js b/src/shared/ssr.js index 3e5a4568e6..5031bfa0e5 100644 --- a/src/shared/ssr.js +++ b/src/shared/ssr.js @@ -54,4 +54,10 @@ export function validateSsrComponent(component, name) { } return component; +} + +export function debug(file, line, column, values) { + console.log(`{@debug} ${file ? file + ' ' : ''}(${line}:${column})`); + console.log(values); + return ''; } \ No newline at end of file diff --git a/test/js/samples/debug-ssr-foo/_config.js b/test/js/samples/debug-ssr-foo/_config.js new file mode 100644 index 0000000000..39bfbe7e62 --- /dev/null +++ b/test/js/samples/debug-ssr-foo/_config.js @@ -0,0 +1,6 @@ +export default { + options: { + generate: 'ssr', + dev: true + } +}; \ No newline at end of file diff --git a/test/js/samples/debug-ssr-foo/expected-bundle.js b/test/js/samples/debug-ssr-foo/expected-bundle.js new file mode 100644 index 0000000000..dcfc5c6d02 --- /dev/null +++ b/test/js/samples/debug-ssr-foo/expected-bundle.js @@ -0,0 +1,46 @@ +var { debug, each, escape } = require("svelte/shared.js"); + +var SvelteComponent = {}; +SvelteComponent.data = function() { + return {}; +}; + +SvelteComponent.render = function(state, options = {}) { + var components = new Set(); + + function addComponent(component) { + components.add(component); + } + + var result = { head: '', addComponent }; + var html = SvelteComponent._render(result, state, options); + + var cssCode = Array.from(components).map(c => c.css && c.css.code).filter(Boolean).join('\n'); + + return { + html, + head: result.head, + css: { code: cssCode, map: null }, + toString() { + return html; + } + }; +}; + +SvelteComponent._render = function(__result, ctx, options) { + __result.addComponent(SvelteComponent); + + ctx = Object.assign({}, ctx); + + return `${ each(ctx.things, item => Object.assign({}, ctx, { thing: item }), ctx => `${escape(ctx.thing.name)} + ${debug(null, 2, 2, { foo: ctx.foo })}`)} + +

foo: ${escape(ctx.foo)}

`; +}; + +SvelteComponent.css = { + code: '', + map: null +}; + +module.exports = SvelteComponent; diff --git a/test/js/samples/debug-ssr-foo/expected.js b/test/js/samples/debug-ssr-foo/expected.js new file mode 100644 index 0000000000..43f1be1db6 --- /dev/null +++ b/test/js/samples/debug-ssr-foo/expected.js @@ -0,0 +1,51 @@ +"use strict"; + +var { debug, each, escape } = require("svelte/shared.js"); + +var SvelteComponent = {};; + +SvelteComponent.data = function() { + return {}; +}; + +SvelteComponent.render = function(state, options = {}) { + var components = new Set(); + + function addComponent(component) { + components.add(component); + } + + var result = { head: '', addComponent }; + var html = SvelteComponent._render(result, state, options); + + var cssCode = Array.from(components).map(c => c.css && c.css.code).filter(Boolean).join('\n'); + + return { + html, + head: result.head, + css: { code: cssCode, map: null }, + toString() { + return html; + } + }; +} + +SvelteComponent._render = function(__result, ctx, options) { + __result.addComponent(SvelteComponent); + + ctx = Object.assign({}, ctx); + + return `${ each(ctx.things, item => Object.assign({}, ctx, { thing: item }), ctx => `${escape(ctx.thing.name)} + ${debug(null, 2, 2, { foo: ctx.foo })}`)} + +

foo: ${escape(ctx.foo)}

`; +}; + +SvelteComponent.css = { + code: '', + map: null +}; + +var warned = false; + +module.exports = SvelteComponent; \ No newline at end of file diff --git a/test/js/samples/debug-ssr-foo/input.html b/test/js/samples/debug-ssr-foo/input.html new file mode 100644 index 0000000000..6e926a4015 --- /dev/null +++ b/test/js/samples/debug-ssr-foo/input.html @@ -0,0 +1,6 @@ +{#each things as thing} + {thing.name} + {@debug foo} +{/each} + +

foo: {foo}

\ No newline at end of file From 4890d4dc02a3f47e52f4675abf1410d893e97781 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Aug 2018 22:55:38 -0400 Subject: [PATCH 05/15] increase timeout, reduce flakiness --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0383d47c81..de91162ece 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ node_js: env: global: - - BUILD_TIMEOUT=10000 + - BUILD_TIMEOUT=20000 addons: apt: From c52a6f011b383532feb91304d2c6bc38e1e20254 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Aug 2018 08:22:41 -0400 Subject: [PATCH 06/15] dont warn on empty block for nbsp - fixes #1658 --- src/validate/html/index.ts | 2 +- test/validator/samples/non-empty-block-dev/_config.js | 3 +++ test/validator/samples/non-empty-block-dev/input.html | 3 +++ test/validator/samples/non-empty-block-dev/warnings.json | 1 + 4 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 test/validator/samples/non-empty-block-dev/_config.js create mode 100644 test/validator/samples/non-empty-block-dev/input.html create mode 100644 test/validator/samples/non-empty-block-dev/warnings.json diff --git a/src/validate/html/index.ts b/src/validate/html/index.ts index ed4eaf4d38..eabe9d8872 100644 --- a/src/validate/html/index.ts +++ b/src/validate/html/index.ts @@ -14,7 +14,7 @@ function isEmptyBlock(node: Node) { if (!/Block$/.test(node.type) || !node.children) return false; if (node.children.length > 1) return false; const child = node.children[0]; - return !child || (child.type === 'Text' && !/\S/.test(child.data)); + return !child || (child.type === 'Text' && !/[^ \r\n\f\v\t]/.test(child.data)); } export default function validateHtml(validator: Validator, html: Node) { diff --git a/test/validator/samples/non-empty-block-dev/_config.js b/test/validator/samples/non-empty-block-dev/_config.js new file mode 100644 index 0000000000..e26996239d --- /dev/null +++ b/test/validator/samples/non-empty-block-dev/_config.js @@ -0,0 +1,3 @@ +export default { + dev: true +}; \ No newline at end of file diff --git a/test/validator/samples/non-empty-block-dev/input.html b/test/validator/samples/non-empty-block-dev/input.html new file mode 100644 index 0000000000..ed4b9e6274 --- /dev/null +++ b/test/validator/samples/non-empty-block-dev/input.html @@ -0,0 +1,3 @@ +{#if x} +   +{/if} \ No newline at end of file diff --git a/test/validator/samples/non-empty-block-dev/warnings.json b/test/validator/samples/non-empty-block-dev/warnings.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/test/validator/samples/non-empty-block-dev/warnings.json @@ -0,0 +1 @@ +[] \ No newline at end of file From 54dc1e58492494debd2e9c39940a45610c6c4b19 Mon Sep 17 00:00:00 2001 From: Christian Kaisermann Date: Fri, 24 Aug 2018 12:16:09 -0300 Subject: [PATCH 07/15] Set style attribute correctly when spread attr present --- src/shared/dom.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/shared/dom.js b/src/shared/dom.js index d428e92f1c..accd2abecf 100644 --- a/src/shared/dom.js +++ b/src/shared/dom.js @@ -87,7 +87,9 @@ export function setAttribute(node, attribute, value) { export function setAttributes(node, attributes) { for (var key in attributes) { - if (key in node) { + if (key === 'style') { + node.style.cssText = attributes[key]; + } else if (key in node) { node[key] = attributes[key]; } else { if (attributes[key] === undefined) removeAttribute(node, key); @@ -238,4 +240,4 @@ export function addResizeListener(element, fn) { element.removeChild(object); } }; -} \ No newline at end of file +} From 5ef44ae6c9b8532f455964c66adb82c443eb0fe6 Mon Sep 17 00:00:00 2001 From: Jacob Wright Date: Fri, 24 Aug 2018 10:09:46 -0600 Subject: [PATCH 08/15] Adds the class directive Allows `
` to simplify templates littered with ternary statements. Addresses #890 --- src/compile/nodes/Class.ts | 18 +++++++ src/compile/nodes/Element.ts | 54 +++++++++++++++++++ src/parse/read/directives.ts | 9 ++++ src/shared/dom.js | 6 ++- src/validate/html/validateComponent.ts | 9 +++- test/runtime/samples/class-boolean/_config.js | 3 ++ test/runtime/samples/class-boolean/main.html | 1 + test/runtime/samples/class-helper/_config.js | 14 +++++ test/runtime/samples/class-helper/main.html | 11 ++++ .../samples/class-with-attribute/_config.js | 3 ++ .../samples/class-with-attribute/main.html | 1 + .../class-with-dynamic-attribute/_config.js | 14 +++++ .../class-with-dynamic-attribute/main.html | 1 + 13 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 src/compile/nodes/Class.ts create mode 100644 test/runtime/samples/class-boolean/_config.js create mode 100644 test/runtime/samples/class-boolean/main.html create mode 100644 test/runtime/samples/class-helper/_config.js create mode 100644 test/runtime/samples/class-helper/main.html create mode 100644 test/runtime/samples/class-with-attribute/_config.js create mode 100644 test/runtime/samples/class-with-attribute/main.html create mode 100644 test/runtime/samples/class-with-dynamic-attribute/_config.js create mode 100644 test/runtime/samples/class-with-dynamic-attribute/main.html diff --git a/src/compile/nodes/Class.ts b/src/compile/nodes/Class.ts new file mode 100644 index 0000000000..965cf7ea0d --- /dev/null +++ b/src/compile/nodes/Class.ts @@ -0,0 +1,18 @@ +import Node from './shared/Node'; +import Expression from './shared/Expression'; + +export default class Class extends Node { + type: 'Class'; + name: string; + expression: Expression; + + constructor(compiler, parent, scope, info) { + super(compiler, parent, scope, info); + + this.name = info.name; + + this.expression = info.expression + ? new Expression(compiler, this, scope, info.expression) + : null; + } +} \ No newline at end of file diff --git a/src/compile/nodes/Element.ts b/src/compile/nodes/Element.ts index 43e690bfd7..bee1103dc0 100644 --- a/src/compile/nodes/Element.ts +++ b/src/compile/nodes/Element.ts @@ -15,6 +15,7 @@ import EventHandler from './EventHandler'; import Transition from './Transition'; import Animation from './Animation'; import Action from './Action'; +import Class from './Class'; import Text from './Text'; import * as namespaces from '../../utils/namespaces'; import mapChildren from './shared/mapChildren'; @@ -68,6 +69,8 @@ export default class Element extends Node { attributes: Attribute[]; actions: Action[]; bindings: Binding[]; + classes: Class[]; + classDependencies: string[]; handlers: EventHandler[]; intro?: Transition; outro?: Transition; @@ -90,6 +93,8 @@ export default class Element extends Node { this.attributes = []; this.actions = []; this.bindings = []; + this.classes = []; + this.classDependencies = []; this.handlers = []; this.intro = null; @@ -144,6 +149,10 @@ export default class Element extends Node { this.bindings.push(new Binding(compiler, this, scope, node)); break; + case 'Class': + this.classes.push(new Class(compiler, this, scope, node)); + break; + case 'EventHandler': this.handlers.push(new EventHandler(compiler, this, scope, node)); break; @@ -228,6 +237,13 @@ export default class Element extends Node { block.addDependencies(binding.value.dependencies); }); + this.classes.forEach(classDir => { + this.parent.cannotUseInnerHTML(); + if (classDir.expression) { + block.addDependencies(classDir.expression.dependencies); + } + }); + this.handlers.forEach(handler => { this.parent.cannotUseInnerHTML(); block.addDependencies(handler.dependencies); @@ -403,6 +419,7 @@ export default class Element extends Node { this.addTransitions(block); this.addAnimation(block); this.addActions(block); + this.addClasses(block); if (this.initialUpdate) { block.builders.mount.addBlock(this.initialUpdate); @@ -584,6 +601,9 @@ export default class Element extends Node { } this.attributes.forEach((attribute: Attribute) => { + if (attribute.name === 'class' && attribute.isDynamic) { + this.classDependencies.push(...attribute.dependencies); + } attribute.render(block); }); } @@ -867,6 +887,26 @@ export default class Element extends Node { }); } + addClasses(block: Block) { + this.classes.forEach(classDir => { + const { expression: { snippet, dependencies}, name } = classDir; + const updater = `@toggleClass(${this.var}, "${name}", ${snippet});`; + + block.builders.hydrate.addLine(updater); + + if ((dependencies && dependencies.size > 0) || this.classDependencies.length) { + const allDeps = this.classDependencies.concat(...dependencies); + const deps = allDeps.map(dependency => `changed.${dependency}`).join(' || '); + const condition = allDeps.length > 1 ? `(${deps})` : deps; + + block.builders.update.addConditional( + condition, + updater + ); + } + }); + } + getStaticAttributeValue(name: string) { const attribute = this.attributes.find( (attr: Attribute) => attr.type === 'Attribute' && attr.name.toLowerCase() === name @@ -937,6 +977,13 @@ export default class Element extends Node { appendTarget.slots[slotName] = ''; } + const classExpr = this.classes.map((classDir: Class) => { + const { expression: { snippet }, name } = classDir; + return `${snippet} ? "${name}" : ""`; + }).join(', '); + + let addClassAttribute = classExpr ? true : false; + if (this.attributes.find(attr => attr.isSpread)) { // TODO dry this out const args = []; @@ -977,12 +1024,19 @@ export default class Element extends Node { ) { // a boolean attribute with one non-Text chunk openingTag += '${' + attribute.chunks[0].snippet + ' ? " ' + attribute.name + '" : "" }'; + } else if (attribute.name === 'class' && classExpr) { + addClassAttribute = false; + openingTag += ` class="\${ [\`${attribute.stringifyForSsr()}\`, ${classExpr} ].join(' ') }"`; } else { openingTag += ` ${attribute.name}="${attribute.stringifyForSsr()}"`; } }); } + if (addClassAttribute) { + openingTag += ` class="\${ [${classExpr}].join(' ') }"`; + } + openingTag += '>'; compiler.target.append(openingTag); diff --git a/src/parse/read/directives.ts b/src/parse/read/directives.ts index a1d71a7ef6..813b1ce54a 100644 --- a/src/parse/read/directives.ts +++ b/src/parse/read/directives.ts @@ -81,6 +81,15 @@ const DIRECTIVES: Record
` +}; diff --git a/test/runtime/samples/class-boolean/main.html b/test/runtime/samples/class-boolean/main.html new file mode 100644 index 0000000000..2595907a6e --- /dev/null +++ b/test/runtime/samples/class-boolean/main.html @@ -0,0 +1 @@ +
diff --git a/test/runtime/samples/class-helper/_config.js b/test/runtime/samples/class-helper/_config.js new file mode 100644 index 0000000000..38aed667be --- /dev/null +++ b/test/runtime/samples/class-helper/_config.js @@ -0,0 +1,14 @@ +export default { + data: { + user: { active: true } + }, + html: `
`, + + test ( assert, component, target, window ) { + component.set({ user: { active: false }}); + + assert.htmlEqual( target.innerHTML, ` +
+ ` ); + } +}; diff --git a/test/runtime/samples/class-helper/main.html b/test/runtime/samples/class-helper/main.html new file mode 100644 index 0000000000..8e77da1282 --- /dev/null +++ b/test/runtime/samples/class-helper/main.html @@ -0,0 +1,11 @@ +
+ + diff --git a/test/runtime/samples/class-with-attribute/_config.js b/test/runtime/samples/class-with-attribute/_config.js new file mode 100644 index 0000000000..719e6bd473 --- /dev/null +++ b/test/runtime/samples/class-with-attribute/_config.js @@ -0,0 +1,3 @@ +export default { + html: `
` +}; diff --git a/test/runtime/samples/class-with-attribute/main.html b/test/runtime/samples/class-with-attribute/main.html new file mode 100644 index 0000000000..c8d9e98f76 --- /dev/null +++ b/test/runtime/samples/class-with-attribute/main.html @@ -0,0 +1 @@ +
diff --git a/test/runtime/samples/class-with-dynamic-attribute/_config.js b/test/runtime/samples/class-with-dynamic-attribute/_config.js new file mode 100644 index 0000000000..9b65c19559 --- /dev/null +++ b/test/runtime/samples/class-with-dynamic-attribute/_config.js @@ -0,0 +1,14 @@ +export default { + data: { + myClass: 'one two' + }, + html: `
`, + + test ( assert, component, target, window ) { + component.set({ myClass: 'one' }); + + assert.htmlEqual( target.innerHTML, ` +
+ ` ); + } +}; diff --git a/test/runtime/samples/class-with-dynamic-attribute/main.html b/test/runtime/samples/class-with-dynamic-attribute/main.html new file mode 100644 index 0000000000..b329f1544d --- /dev/null +++ b/test/runtime/samples/class-with-dynamic-attribute/main.html @@ -0,0 +1 @@ +
From 96c4455af933636e3450df8386d65fabb7b4db68 Mon Sep 17 00:00:00 2001 From: Jacob Wright Date: Fri, 24 Aug 2018 14:01:51 -0600 Subject: [PATCH 09/15] Fixes an error with outros and elseifs This is a fix for when an elseif doesn't have a final else, and the following error was thrown: ``` TypeError: Cannot read property 'o' of undefined ``` See https://svelte.technology/repl?version=2.12.0&gist=c33d308077447f8ba06b79d8ef5ab1e4 --- src/compile/nodes/IfBlock.ts | 7 ++++--- .../if-block-outro-nested-else/Component.html | 1 + .../if-block-outro-nested-else/_config.js | 8 ++++++++ .../samples/if-block-outro-nested-else/main.html | 16 ++++++++++++++++ 4 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 test/runtime/samples/if-block-outro-nested-else/Component.html create mode 100644 test/runtime/samples/if-block-outro-nested-else/_config.js create mode 100644 test/runtime/samples/if-block-outro-nested-else/main.html diff --git a/src/compile/nodes/IfBlock.ts b/src/compile/nodes/IfBlock.ts index 2a5a49af16..eef654b90b 100644 --- a/src/compile/nodes/IfBlock.ts +++ b/src/compile/nodes/IfBlock.ts @@ -139,9 +139,10 @@ export default class IfBlock extends Node { this.buildCompoundWithOutros(block, parentNode, parentNodes, branches, dynamic, vars); if (this.compiler.options.nestedTransitions) { - block.builders.outro.addLine( - `${name}.o(#outrocallback);` - ); + block.builders.outro.addBlock(deindent` + if (${name}) ${name}.o(#outrocallback); + else #outrocallback(); + `); } } else { this.buildCompound(block, parentNode, parentNodes, branches, dynamic, vars); diff --git a/test/runtime/samples/if-block-outro-nested-else/Component.html b/test/runtime/samples/if-block-outro-nested-else/Component.html new file mode 100644 index 0000000000..281c6866c3 --- /dev/null +++ b/test/runtime/samples/if-block-outro-nested-else/Component.html @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/test/runtime/samples/if-block-outro-nested-else/_config.js b/test/runtime/samples/if-block-outro-nested-else/_config.js new file mode 100644 index 0000000000..4fe5eaa926 --- /dev/null +++ b/test/runtime/samples/if-block-outro-nested-else/_config.js @@ -0,0 +1,8 @@ +export default { + nestedTransitions: true, + + test ( assert, component, target ) { + // Would cause "TypeError: Cannot read property 'o' of undefined" + component.set({ foo: false }); + } +}; diff --git a/test/runtime/samples/if-block-outro-nested-else/main.html b/test/runtime/samples/if-block-outro-nested-else/main.html new file mode 100644 index 0000000000..02e000b3c3 --- /dev/null +++ b/test/runtime/samples/if-block-outro-nested-else/main.html @@ -0,0 +1,16 @@ +{#if foo} + {#if false} + + {:elseif false} + + {/if} +{/if} + + From e26dcadecfdc678e40382c863e6f35c93277db68 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Aug 2018 19:52:47 -0400 Subject: [PATCH 10/15] -> v2.12.1 --- CHANGELOG.md | 11 +++++++++++ package.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9c18fafd1..fddab48189 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Svelte changelog +## 2.12.1 + +* Allow actions to take any expression ([#1676](https://github.com/sveltejs/svelte/issues/1676)) +* Run transitions in component context ([#1675](https://github.com/sveltejs/svelte/issues/1675)) +* Correctly set select value on mount ([#1666](https://github.com/sveltejs/svelte/issues/1666)) +* Support `{@debug}` in SSR ([#1659](https://github.com/sveltejs/svelte/issues/1659)) +* Don't treat ` ` as empty whitespace ([#1658](https://github.com/sveltejs/svelte/issues/1658)) +* Fix outros for if blocks with no else ([#1688](https://github.com/sveltejs/svelte/pull/1688)) +* Set `style.cssText` in spread attributes ([#1684](https://github.com/sveltejs/svelte/pull/1684)) + + ## 2.12.0 * Initialise actions on mount rather than hydrate ([#1653](https://github.com/sveltejs/svelte/pull/1653)) diff --git a/package.json b/package.json index 8bd1fc7b08..905bfae7f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte", - "version": "2.12.0", + "version": "2.12.1", "description": "The magical disappearing UI framework", "main": "compiler/svelte.js", "bin": { From d0d548f7084aa13fdc7900416527adedc61f115c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Aug 2018 20:00:05 -0400 Subject: [PATCH 11/15] remove sourcemaps from package --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 905bfae7f8..740743048b 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ }, "files": [ "cli", - "compiler", - "ssr", + "compiler/*.js", + "ssr/*.js", "shared.js", "store.js", "store.umd.js", From f12141e18a34f828dd3697aa4151af17e83fc8b2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Aug 2018 21:32:35 -0400 Subject: [PATCH 12/15] Update dom.js --- src/shared/dom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/dom.js b/src/shared/dom.js index 749695dbb9..2952755038 100644 --- a/src/shared/dom.js +++ b/src/shared/dom.js @@ -243,5 +243,5 @@ export function addResizeListener(element, fn) { } export function toggleClass(element, name, toggle) { - element.classList.toggle(name, Boolean(toggle)); + element.classList.toggle(name, toggle); } From c7609a2da3a1a38d1a008a3d9a1ebbfd3935100f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 24 Aug 2018 21:42:29 -0400 Subject: [PATCH 13/15] -> v2.13.0 --- CHANGELOG.md | 5 +++++ package.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fddab48189..dbb1be91af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Svelte changelog +## 2.13.0 + +* Add `class` directive ([#890](https://github.com/sveltejs/svelte/issues/890)) +* Remove sourcemaps from npm package ([#1690](https://github.com/sveltejs/svelte/pull/1690)) + ## 2.12.1 * Allow actions to take any expression ([#1676](https://github.com/sveltejs/svelte/issues/1676)) diff --git a/package.json b/package.json index 740743048b..18dbdabfb0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte", - "version": "2.12.1", + "version": "2.13.0", "description": "The magical disappearing UI framework", "main": "compiler/svelte.js", "bin": { From 4d4c2c3655f5b324031f25eeaa012655ce039e15 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Aug 2018 06:27:38 -0400 Subject: [PATCH 14/15] coerce toggle argument --- src/shared/dom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/dom.js b/src/shared/dom.js index 2952755038..b57a19600a 100644 --- a/src/shared/dom.js +++ b/src/shared/dom.js @@ -243,5 +243,5 @@ export function addResizeListener(element, fn) { } export function toggleClass(element, name, toggle) { - element.classList.toggle(name, toggle); + element.classList.toggle(name, !!toggle); } From 0f171a5f3efa1c9296992901ba2b949db7aa2677 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Aug 2018 06:29:23 -0400 Subject: [PATCH 15/15] -> v2.13.1 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbb1be91af..5ccd6be704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Svelte changelog +## 2.13.1 + +* Coerce second argument to `toggleClass` ([#1685](https://github.com/sveltejs/svelte/issues/1685)) + ## 2.13.0 * Add `class` directive ([#890](https://github.com/sveltejs/svelte/issues/890)) diff --git a/package.json b/package.json index 18dbdabfb0..b245b1f88e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelte", - "version": "2.13.0", + "version": "2.13.1", "description": "The magical disappearing UI framework", "main": "compiler/svelte.js", "bin": {