Merge pull request #627 from sveltejs/gh-624

allow components to have computed member expressions for bindings
pull/636/head
Rich Harris 8 years ago committed by GitHub
commit bb17940b7e

@ -5,6 +5,7 @@ import { DomGenerator } from '../../index';
import Block from '../../Block'; import Block from '../../Block';
import { Node } from '../../../../interfaces'; import { Node } from '../../../../interfaces';
import { State } from '../../interfaces'; import { State } from '../../interfaces';
import getObject from '../../../../utils/getObject';
export default function visitBinding( export default function visitBinding(
generator: DomGenerator, generator: DomGenerator,
@ -14,16 +15,11 @@ export default function visitBinding(
attribute, attribute,
local local
) { ) {
const { name } = flattenReference(attribute.value); const { name } = getObject(attribute.value);
const { snippet, contexts, dependencies } = block.contextualise( const { snippet, contexts, dependencies } = block.contextualise(
attribute.value 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 => { contexts.forEach(context => {
if (!~local.allUsedContexts.indexOf(context)) if (!~local.allUsedContexts.indexOf(context))
local.allUsedContexts.push(context); local.allUsedContexts.push(context);
@ -38,8 +34,9 @@ export default function visitBinding(
obj = block.listNames.get(name); obj = block.listNames.get(name);
prop = block.indexNames.get(name); prop = block.indexNames.get(name);
} else if (attribute.value.type === 'MemberExpression') { } else if (attribute.value.type === 'MemberExpression') {
prop = `'[✂${attribute.value.property.start}-${attribute.value.property prop = `[✂${attribute.value.property.start}-${attribute.value.property
.end}]'`; .end}]`;
if (!attribute.value.computed) prop = `'${prop}'`;
obj = `[✂${attribute.value.object.start}-${attribute.value.object.end}✂]`; obj = `[✂${attribute.value.object.start}-${attribute.value.object.end}✂]`;
} else { } else {
obj = 'state'; obj = 'state';
@ -85,7 +82,7 @@ export default function visitBinding(
local.update.addBlock(deindent` local.update.addBlock(deindent`
if ( !${updating} && ${dependencies if ( !${updating} && ${dependencies
.map(dependency => `'${dependency}' in changed`) .map(dependency => `'${dependency}' in changed`)
.join('||')} ) { .join(' || ')} ) {
${updating} = true; ${updating} = true;
${local.name}._set({ ${attribute.name}: ${snippet} }); ${local.name}._set({ ${attribute.name}: ${snippet} });
${updating} = false; ${updating} = false;

@ -6,12 +6,7 @@ import { DomGenerator } from '../../index';
import Block from '../../Block'; import Block from '../../Block';
import { Node } from '../../../../interfaces'; import { Node } from '../../../../interfaces';
import { State } from '../../interfaces'; import { State } from '../../interfaces';
import getObject from '../../../../utils/getObject';
function getObject(node) {
// TODO validation should ensure this is an Identifier or a MemberExpression
while (node.type === 'MemberExpression') node = node.object;
return node;
}
export default function visitBinding( export default function visitBinding(
generator: DomGenerator, generator: DomGenerator,
@ -21,15 +16,7 @@ export default function visitBinding(
attribute: Node attribute: Node
) { ) {
const { name } = getObject(attribute.value); const { name } = getObject(attribute.value);
const { snippet, contexts } = block.contextualise(attribute.value); const { snippet, contexts, dependencies } = 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!'
);
contexts.forEach(context => { contexts.forEach(context => {
if (!~state.allUsedContexts.indexOf(context)) if (!~state.allUsedContexts.indexOf(context))

@ -1,4 +1,6 @@
import deindent from '../../../../../utils/deindent'; import deindent from '../../../../../utils/deindent';
import getTailSnippet from '../../../../../utils/getTailSnippet';
import { Node } from '../../../../../interfaces';
export default function getSetter({ export default function getSetter({
block, block,
@ -40,15 +42,7 @@ export default function getSetter({
return `${block.component}._set({ ${name}: ${value} });`; return `${block.component}._set({ ${name}: ${value} });`;
} }
function getTailSnippet(node) { function isComputed(node: Node) {
const end = node.end;
while (node.type === 'MemberExpression') node = node.object;
const start = node.end;
return `[✂${start}-${end}✂]`;
}
function isComputed(node) {
while (node.type === 'MemberExpression') { while (node.type === 'MemberExpression') {
if (node.computed) return true; if (node.computed) return true;
node = node.object; node = node.object;

@ -2,6 +2,7 @@ import deindent from '../../utils/deindent';
import flattenReference from '../../utils/flattenReference'; import flattenReference from '../../utils/flattenReference';
import { SsrGenerator } from './index'; import { SsrGenerator } from './index';
import { Node } from '../../interfaces'; import { Node } from '../../interfaces';
import getObject from '../../utils/getObject';
interface BlockOptions { interface BlockOptions {
// TODO // TODO
@ -25,13 +26,13 @@ export default class Block {
this.conditions.map(c => `(${c})`) this.conditions.map(c => `(${c})`)
); );
const { keypath } = flattenReference(binding.value); const { name: prop } = getObject(binding.value);
this.generator.bindings.push(deindent` this.generator.bindings.push(deindent`
if ( ${conditions.join('&&')} ) { if ( ${conditions.join('&&')} ) {
tmp = ${name}.data(); tmp = ${name}.data();
if ( '${keypath}' in tmp ) { if ( '${prop}' in tmp ) {
state.${binding.name} = tmp.${keypath}; state.${binding.name} = tmp.${prop};
settled = false; settled = false;
} }
} }

@ -3,6 +3,8 @@ import visit from '../visit';
import { SsrGenerator } from '../index'; import { SsrGenerator } from '../index';
import Block from '../Block'; import Block from '../Block';
import { Node } from '../../../interfaces'; import { Node } from '../../../interfaces';
import getObject from '../../../utils/getObject';
import getTailSnippet from '../../../utils/getTailSnippet';
export default function visitComponent( export default function visitComponent(
generator: SsrGenerator, generator: SsrGenerator,
@ -52,9 +54,13 @@ export default function visitComponent(
}) })
.concat( .concat(
bindings.map(binding => { bindings.map(binding => {
const { name, keypath } = flattenReference(binding.value); const { name } = getObject(binding.value);
const value = block.contexts.has(name) ? keypath : `state.${keypath}`; const tail = binding.value.type === 'MemberExpression'
return `${binding.name}: ${value}`; ? getTailSnippet(binding.value)
: '';
const keypath = block.contexts.has(name) ? `${name}${tail}` : `state.${name}${tail}`;
return `${binding.name}: ${keypath}`;
}) })
) )
.join(', '); .join(', ');

@ -0,0 +1,6 @@
import { Node } from '../interfaces';
export default function getObject(node: Node) {
while (node.type === 'MemberExpression') node = node.object;
return node;
}

@ -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}✂]`;
}

@ -2,6 +2,7 @@ import jsdom from 'jsdom';
import assert from 'assert'; import assert from 'assert';
import glob from 'glob'; import glob from 'glob';
import fs from 'fs'; import fs from 'fs';
import path from 'path';
import chalk from 'chalk'; import chalk from 'chalk';
import * as consoleGroup from 'console-group'; import * as consoleGroup from 'console-group';
@ -162,13 +163,17 @@ export function addLineNumbers(code) {
.join('\n'); .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 => { glob.sync('**/*.html', { cwd }).forEach(file => {
const { code } = svelte.compile( const { code } = svelte.compile(
fs.readFileSync(`${cwd}/${file}`, 'utf-8'), fs.readFileSync(`${cwd}/${file}`, 'utf-8'),
{ Object.assign(options, {
shared name: capitalize(file.slice(0, -path.extname(file).length))
} })
); );
console.log( // eslint-disable-line no-console console.log( // eslint-disable-line no-console

@ -57,41 +57,26 @@ describe("runtime", () => {
throw new Error("Forgot to remove `solo: true` from test"); 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)) { if (failed.has(dir)) {
// this makes debugging easier, by only printing compiled output once // this makes debugging easier, by only printing compiled output once
throw new Error('skipping inline helpers test'); throw new Error('skipping inline helpers test');
} }
const cwd = path.resolve(`test/runtime/samples/${dir}`); const cwd = path.resolve(`test/runtime/samples/${dir}`);
let compiled;
compileOptions = config.compileOptions || {}; compileOptions = config.compileOptions || {};
compileOptions.shared = shared; compileOptions.shared = shared;
compileOptions.dev = config.dev; compileOptions.dev = config.dev;
// check that no ES2015+ syntax slipped in
if (!config.allowES2015) {
try { try {
const source = fs.readFileSync( const source = fs.readFileSync(
`test/runtime/samples/${dir}/main.html`, `test/runtime/samples/${dir}/main.html`,
"utf-8" "utf-8"
); );
compiled = svelte.compile(source, compileOptions); const { code } = 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 startIndex = code.indexOf("function create_main_fragment"); // may change! const startIndex = code.indexOf("function create_main_fragment"); // may change!
if (startIndex === -1) if (startIndex === -1)
throw new Error("missing create_main_fragment"); throw new Error("missing create_main_fragment");
@ -101,7 +86,7 @@ describe("runtime", () => {
acorn.parse(es5, { ecmaVersion: 5 }); acorn.parse(es5, { ecmaVersion: 5 });
} catch (err) { } catch (err) {
failed.add(dir); failed.add(dir);
showOutput(cwd, shared); // eslint-disable-line no-console showOutput(cwd, { shared }); // eslint-disable-line no-console
throw err; throw err;
} }
} }
@ -146,7 +131,7 @@ describe("runtime", () => {
try { try {
SvelteComponent = require(`./samples/${dir}/main.html`).default; SvelteComponent = require(`./samples/${dir}/main.html`).default;
} catch (err) { } catch (err) {
showOutput(cwd, shared); // eslint-disable-line no-console showOutput(cwd, { shared }); // eslint-disable-line no-console
throw err; throw err;
} }
@ -210,12 +195,12 @@ describe("runtime", () => {
config.error(assert, err); config.error(assert, err);
} else { } else {
failed.add(dir); failed.add(dir);
showOutput(cwd, shared); // eslint-disable-line no-console showOutput(cwd, { shared }); // eslint-disable-line no-console
throw err; throw err;
} }
}) })
.then(() => { .then(() => {
if (config.show) showOutput(cwd, shared); if (config.show) showOutput(cwd, { shared });
}); });
}); });
} }

@ -0,0 +1,3 @@
<label>
{{field}} <input bind:value>
</label>

@ -0,0 +1,34 @@
export default {
html: `
<label>firstname <input></label>
<label>lastname <input></label>
`,
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');
}
};

@ -0,0 +1,22 @@
{{#each fields as field}}
<Nested :field bind:value='values[field]'/>
{{/each}}
<script>
import Nested from './Nested.html';
export default {
data: function () {
return {
fields: ['firstname', 'lastname'],
values: {
firstname: '',
lastname: ''
}
};
},
components: {
Nested
}
};
</script>

@ -3,10 +3,9 @@ import * as fs from "fs";
import * as path from "path"; import * as path from "path";
import { import {
addLineNumbers, showOutput,
loadConfig, loadConfig,
setupHtmlEqual, setupHtmlEqual,
svelte,
tryToLoadJson tryToLoadJson
} from "../helpers.js"; } from "../helpers.js";
@ -19,10 +18,6 @@ function tryToReadFile(file) {
} }
} }
function capitalize(str) {
return str[0].toUpperCase() + str.slice(1);
}
describe("ssr", () => { describe("ssr", () => {
before(() => { before(() => {
require(process.env.COVERAGE require(process.env.COVERAGE
@ -64,18 +59,7 @@ describe("ssr", () => {
error = e; error = e;
} }
if (show) { if (show) showOutput(dir, { generate: "ssr" });
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 (error) throw error; if (error) throw error;
fs.writeFileSync(`${dir}/_actual.html`, html); fs.writeFileSync(`${dir}/_actual.html`, html);
@ -102,22 +86,7 @@ describe("ssr", () => {
if (config["skip-ssr"]) return; if (config["skip-ssr"]) return;
(config.skip ? it.skip : config.solo ? it.only : it)(dir, () => { (config.skip ? it.skip : config.solo ? it.only : it)(dir, () => {
let compiled; const cwd = path.resolve("test/runtime/samples", dir);
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;
}
}
fs.readdirSync(`test/runtime/samples/${dir}`).forEach(file => { fs.readdirSync(`test/runtime/samples/${dir}`).forEach(file => {
const resolved = require.resolve(`../runtime/samples/${dir}/${file}`); const resolved = require.resolve(`../runtime/samples/${dir}/${file}`);
@ -134,7 +103,7 @@ describe("ssr", () => {
assert.htmlEqual(html, config.html); assert.htmlEqual(html, config.html);
} }
} catch (err) { } catch (err) {
console.log(addLineNumbers(compiled.code)); // eslint-disable-line no-console showOutput(cwd, { generate: "ssr" });
throw err; throw err;
} }
}); });

Loading…
Cancel
Save