diff --git a/src/generators/dom/visitors/Component/Binding.ts b/src/generators/dom/visitors/Component/Binding.ts index fa7d539a4242..ac8e90f0f221 100644 --- a/src/generators/dom/visitors/Component/Binding.ts +++ b/src/generators/dom/visitors/Component/Binding.ts @@ -5,6 +5,7 @@ import { DomGenerator } from '../../index'; import Block from '../../Block'; import { Node } from '../../../../interfaces'; import { State } from '../../interfaces'; +import getObject from '../../../../utils/getObject'; export default function visitBinding( generator: DomGenerator, @@ -14,16 +15,11 @@ export default function visitBinding( attribute, local ) { - const { name } = flattenReference(attribute.value); + const { name } = getObject(attribute.value); const { snippet, contexts, dependencies } = block.contextualise( attribute.value ); - if (dependencies.length > 1) - throw new Error( - 'An unexpected situation arose. Please raise an issue at https://github.com/sveltejs/svelte/issues — thanks!' - ); - contexts.forEach(context => { if (!~local.allUsedContexts.indexOf(context)) local.allUsedContexts.push(context); @@ -38,8 +34,9 @@ export default function visitBinding( obj = block.listNames.get(name); prop = block.indexNames.get(name); } else if (attribute.value.type === 'MemberExpression') { - prop = `'[✂${attribute.value.property.start}-${attribute.value.property - .end}✂]'`; + prop = `[✂${attribute.value.property.start}-${attribute.value.property + .end}✂]`; + if (!attribute.value.computed) prop = `'${prop}'`; obj = `[✂${attribute.value.object.start}-${attribute.value.object.end}✂]`; } else { obj = 'state'; @@ -85,7 +82,7 @@ export default function visitBinding( local.update.addBlock(deindent` if ( !${updating} && ${dependencies .map(dependency => `'${dependency}' in changed`) - .join('||')} ) { + .join(' || ')} ) { ${updating} = true; ${local.name}._set({ ${attribute.name}: ${snippet} }); ${updating} = false; diff --git a/src/generators/dom/visitors/Element/Binding.ts b/src/generators/dom/visitors/Element/Binding.ts index fdac104a8743..7963677d19a9 100644 --- a/src/generators/dom/visitors/Element/Binding.ts +++ b/src/generators/dom/visitors/Element/Binding.ts @@ -6,12 +6,7 @@ import { DomGenerator } from '../../index'; import Block from '../../Block'; import { Node } from '../../../../interfaces'; import { State } from '../../interfaces'; - -function getObject(node) { - // TODO validation should ensure this is an Identifier or a MemberExpression - while (node.type === 'MemberExpression') node = node.object; - return node; -} +import getObject from '../../../../utils/getObject'; export default function visitBinding( generator: DomGenerator, @@ -21,15 +16,7 @@ export default function visitBinding( attribute: Node ) { const { name } = getObject(attribute.value); - const { snippet, contexts } = block.contextualise(attribute.value); - const dependencies = block.contextDependencies.has(name) - ? block.contextDependencies.get(name) - : [name]; - - if (dependencies.length > 1) - throw new Error( - 'An unexpected situation arose. Please raise an issue at https://github.com/sveltejs/svelte/issues — thanks!' - ); + const { snippet, contexts, dependencies } = block.contextualise(attribute.value); contexts.forEach(context => { if (!~state.allUsedContexts.indexOf(context)) diff --git a/src/generators/dom/visitors/shared/binding/getSetter.ts b/src/generators/dom/visitors/shared/binding/getSetter.ts index 5cb4cc4c6251..7ce77b448907 100644 --- a/src/generators/dom/visitors/shared/binding/getSetter.ts +++ b/src/generators/dom/visitors/shared/binding/getSetter.ts @@ -1,4 +1,6 @@ import deindent from '../../../../../utils/deindent'; +import getTailSnippet from '../../../../../utils/getTailSnippet'; +import { Node } from '../../../../../interfaces'; export default function getSetter({ block, @@ -40,15 +42,7 @@ export default function getSetter({ return `${block.component}._set({ ${name}: ${value} });`; } -function getTailSnippet(node) { - const end = node.end; - while (node.type === 'MemberExpression') node = node.object; - const start = node.end; - - return `[✂${start}-${end}✂]`; -} - -function isComputed(node) { +function isComputed(node: Node) { while (node.type === 'MemberExpression') { if (node.computed) return true; node = node.object; diff --git a/src/generators/server-side-rendering/Block.ts b/src/generators/server-side-rendering/Block.ts index 1ad36fff5a2a..8d908ca8eac3 100644 --- a/src/generators/server-side-rendering/Block.ts +++ b/src/generators/server-side-rendering/Block.ts @@ -2,6 +2,7 @@ import deindent from '../../utils/deindent'; import flattenReference from '../../utils/flattenReference'; import { SsrGenerator } from './index'; import { Node } from '../../interfaces'; +import getObject from '../../utils/getObject'; interface BlockOptions { // TODO @@ -25,13 +26,13 @@ export default class Block { this.conditions.map(c => `(${c})`) ); - const { keypath } = flattenReference(binding.value); + const { name: prop } = getObject(binding.value); this.generator.bindings.push(deindent` if ( ${conditions.join('&&')} ) { tmp = ${name}.data(); - if ( '${keypath}' in tmp ) { - state.${binding.name} = tmp.${keypath}; + if ( '${prop}' in tmp ) { + state.${binding.name} = tmp.${prop}; settled = false; } } diff --git a/src/generators/server-side-rendering/visitors/Component.ts b/src/generators/server-side-rendering/visitors/Component.ts index 05523a29a879..6fb87973e418 100644 --- a/src/generators/server-side-rendering/visitors/Component.ts +++ b/src/generators/server-side-rendering/visitors/Component.ts @@ -3,6 +3,8 @@ import visit from '../visit'; import { SsrGenerator } from '../index'; import Block from '../Block'; import { Node } from '../../../interfaces'; +import getObject from '../../../utils/getObject'; +import getTailSnippet from '../../../utils/getTailSnippet'; export default function visitComponent( generator: SsrGenerator, @@ -52,9 +54,13 @@ export default function visitComponent( }) .concat( bindings.map(binding => { - const { name, keypath } = flattenReference(binding.value); - const value = block.contexts.has(name) ? keypath : `state.${keypath}`; - return `${binding.name}: ${value}`; + const { name } = getObject(binding.value); + const tail = binding.value.type === 'MemberExpression' + ? getTailSnippet(binding.value) + : ''; + + const keypath = block.contexts.has(name) ? `${name}${tail}` : `state.${name}${tail}`; + return `${binding.name}: ${keypath}`; }) ) .join(', '); diff --git a/src/utils/deindent.ts b/src/utils/deindent.ts index 13278b26b18e..5f42e896041c 100644 --- a/src/utils/deindent.ts +++ b/src/utils/deindent.ts @@ -1,6 +1,6 @@ const start = /\n(\t+)/; -export default function deindent(strings: string[], ...values: any[]) { +export default function deindent(strings: TemplateStringsArray, ...values: any[]) { const indentation = start.exec(strings[0])[1]; const pattern = new RegExp(`^${indentation}`, 'gm'); diff --git a/src/utils/getObject.ts b/src/utils/getObject.ts new file mode 100644 index 000000000000..0d3ce4d789e8 --- /dev/null +++ b/src/utils/getObject.ts @@ -0,0 +1,6 @@ +import { Node } from '../interfaces'; + +export default function getObject(node: Node) { + while (node.type === 'MemberExpression') node = node.object; + return node; +} \ No newline at end of file diff --git a/src/utils/getTailSnippet.ts b/src/utils/getTailSnippet.ts new file mode 100644 index 000000000000..499e705ec65f --- /dev/null +++ b/src/utils/getTailSnippet.ts @@ -0,0 +1,9 @@ +import { Node } from '../interfaces'; + +export default function getTailSnippet(node: Node) { + const end = node.end; + while (node.type === 'MemberExpression') node = node.object; + const start = node.end; + + return `[✂${start}-${end}✂]`; +} \ No newline at end of file diff --git a/test/helpers.js b/test/helpers.js index 1d2d6c10890b..33131da5585e 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -2,6 +2,7 @@ import jsdom from 'jsdom'; import assert from 'assert'; import glob from 'glob'; import fs from 'fs'; +import path from 'path'; import chalk from 'chalk'; import * as consoleGroup from 'console-group'; @@ -162,13 +163,17 @@ export function addLineNumbers(code) { .join('\n'); } -export function showOutput(cwd, shared) { +function capitalize(str) { + return str[0].toUpperCase() + str.slice(1); +} + +export function showOutput(cwd, options) { glob.sync('**/*.html', { cwd }).forEach(file => { const { code } = svelte.compile( fs.readFileSync(`${cwd}/${file}`, 'utf-8'), - { - shared - } + Object.assign(options, { + name: capitalize(file.slice(0, -path.extname(file).length)) + }) ); console.log( // eslint-disable-line no-console diff --git a/test/runtime/index.js b/test/runtime/index.js index a21648b60410..222ac8b5fbc6 100644 --- a/test/runtime/index.js +++ b/test/runtime/index.js @@ -57,41 +57,26 @@ describe("runtime", () => { throw new Error("Forgot to remove `solo: true` from test"); } - (config.skip ? it.skip : config.solo ? it.only : it)(`${dir} (${shared ? 'shared' : 'inline'} helpers`, () => { + (config.skip ? it.skip : config.solo ? it.only : it)(`${dir} (${shared ? 'shared' : 'inline'} helpers)`, () => { if (failed.has(dir)) { // this makes debugging easier, by only printing compiled output once throw new Error('skipping inline helpers test'); } const cwd = path.resolve(`test/runtime/samples/${dir}`); - let compiled; compileOptions = config.compileOptions || {}; compileOptions.shared = shared; compileOptions.dev = config.dev; - try { - const source = fs.readFileSync( - `test/runtime/samples/${dir}/main.html`, - "utf-8" - ); - compiled = svelte.compile(source, compileOptions); - } catch (err) { - if (config.compileError) { - config.compileError(err); - return; - } else { - failed.add(dir); - showOutput(cwd, shared); - throw err; - } - } - - const { code } = compiled; - // check that no ES2015+ syntax slipped in if (!config.allowES2015) { try { + const source = fs.readFileSync( + `test/runtime/samples/${dir}/main.html`, + "utf-8" + ); + const { code } = svelte.compile(source, compileOptions); const startIndex = code.indexOf("function create_main_fragment"); // may change! if (startIndex === -1) throw new Error("missing create_main_fragment"); @@ -101,7 +86,7 @@ describe("runtime", () => { acorn.parse(es5, { ecmaVersion: 5 }); } catch (err) { failed.add(dir); - showOutput(cwd, shared); // eslint-disable-line no-console + showOutput(cwd, { shared }); // eslint-disable-line no-console throw err; } } @@ -146,7 +131,7 @@ describe("runtime", () => { try { SvelteComponent = require(`./samples/${dir}/main.html`).default; } catch (err) { - showOutput(cwd, shared); // eslint-disable-line no-console + showOutput(cwd, { shared }); // eslint-disable-line no-console throw err; } @@ -210,12 +195,12 @@ describe("runtime", () => { config.error(assert, err); } else { failed.add(dir); - showOutput(cwd, shared); // eslint-disable-line no-console + showOutput(cwd, { shared }); // eslint-disable-line no-console throw err; } }) .then(() => { - if (config.show) showOutput(cwd, shared); + if (config.show) showOutput(cwd, { shared }); }); }); } diff --git a/test/runtime/samples/component-binding-computed/Nested.html b/test/runtime/samples/component-binding-computed/Nested.html new file mode 100644 index 000000000000..ca75184db2ac --- /dev/null +++ b/test/runtime/samples/component-binding-computed/Nested.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/test/runtime/samples/component-binding-computed/_config.js b/test/runtime/samples/component-binding-computed/_config.js new file mode 100644 index 000000000000..08590c49543f --- /dev/null +++ b/test/runtime/samples/component-binding-computed/_config.js @@ -0,0 +1,34 @@ +export default { + html: ` + + + `, + + test ( assert, component, target, window ) { + const input = new window.Event( 'input' ); + const inputs = target.querySelectorAll( 'input' ); + + inputs[0].value = 'Ada'; + inputs[0].dispatchEvent(input); + assert.deepEqual(component.get('values'), { + firstname: 'Ada', + lastname: '' + }); + + inputs[1].value = 'Lovelace'; + inputs[1].dispatchEvent(input); + assert.deepEqual(component.get('values'), { + firstname: 'Ada', + lastname: 'Lovelace' + }); + + component.set({ + values: { + firstname: 'Grace', + lastname: 'Hopper' + } + }); + assert.equal(inputs[0].value, 'Grace'); + assert.equal(inputs[1].value, 'Hopper'); + } +}; diff --git a/test/runtime/samples/component-binding-computed/main.html b/test/runtime/samples/component-binding-computed/main.html new file mode 100644 index 000000000000..9ac402fec8ea --- /dev/null +++ b/test/runtime/samples/component-binding-computed/main.html @@ -0,0 +1,22 @@ +{{#each fields as field}} + +{{/each}} + + \ No newline at end of file diff --git a/test/server-side-rendering/index.js b/test/server-side-rendering/index.js index 02f4a50e9f60..e70bb13e734a 100644 --- a/test/server-side-rendering/index.js +++ b/test/server-side-rendering/index.js @@ -3,10 +3,9 @@ import * as fs from "fs"; import * as path from "path"; import { - addLineNumbers, + showOutput, loadConfig, setupHtmlEqual, - svelte, tryToLoadJson } from "../helpers.js"; @@ -19,10 +18,6 @@ function tryToReadFile(file) { } } -function capitalize(str) { - return str[0].toUpperCase() + str.slice(1); -} - describe("ssr", () => { before(() => { require(process.env.COVERAGE @@ -64,18 +59,7 @@ describe("ssr", () => { error = e; } - if (show) { - fs.readdirSync(dir).forEach(file => { - if (file[0] === "_") return; - const source = fs.readFileSync(`${dir}/${file}`, "utf-8"); - const name = capitalize(file.slice(0, -path.extname(file).length)); - const { code } = svelte.compile(source, { generate: "ssr", name }); - console.group(file); - console.log(addLineNumbers(code)); - console.groupEnd(); - }); - } - + if (show) showOutput(dir, { generate: "ssr" }); if (error) throw error; fs.writeFileSync(`${dir}/_actual.html`, html); @@ -102,22 +86,7 @@ describe("ssr", () => { if (config["skip-ssr"]) return; (config.skip ? it.skip : config.solo ? it.only : it)(dir, () => { - let compiled; - - try { - const source = fs.readFileSync( - `test/runtime/samples/${dir}/main.html`, - "utf-8" - ); - compiled = svelte.compile(source, { generate: "ssr" }); - } catch (err) { - if (config.compileError) { - config.compileError(err); - return; - } else { - throw err; - } - } + const cwd = path.resolve("test/runtime/samples", dir); fs.readdirSync(`test/runtime/samples/${dir}`).forEach(file => { const resolved = require.resolve(`../runtime/samples/${dir}/${file}`); @@ -134,7 +103,7 @@ describe("ssr", () => { assert.htmlEqual(html, config.html); } } catch (err) { - console.log(addLineNumbers(compiled.code)); // eslint-disable-line no-console + showOutput(cwd, { generate: "ssr" }); throw err; } });