Merge branch 'master' into gh-1122

pull/1173/head
Rich Harris 8 years ago
commit dd5e0f7358

@ -137,27 +137,21 @@ export default class Component extends Node {
block.addVariable(name_updating, '{}'); block.addVariable(name_updating, '{}');
statements.push(`var ${name_initial_data} = ${initialPropString};`); statements.push(`var ${name_initial_data} = ${initialPropString};`);
const setParentFromChildOnChange = new CodeBuilder(); let hasLocalBindings = false;
const setParentFromChildOnInit = new CodeBuilder(); let hasStoreBindings = false;
const setStoreFromChildOnChange = new CodeBuilder(); const builder = new CodeBuilder();
const setStoreFromChildOnInit = new CodeBuilder();
bindings.forEach((binding: Binding) => { bindings.forEach((binding: Binding) => {
let { name: key } = getObject(binding.value); let { name: key } = getObject(binding.value);
const isStoreProp = generator.options.store && key[0] === '$';
if (isStoreProp) key = key.slice(1);
const newState = isStoreProp ? 'newStoreState' : 'newState';
binding.contexts.forEach(context => { binding.contexts.forEach(context => {
allContexts.add(context); allContexts.add(context);
}); });
let setFromChild; let setFromChild;
if (!isStoreProp && block.contexts.has(key)) { if (block.contexts.has(key)) {
const prop = binding.dependencies[0];
const computed = isComputed(binding.value); const computed = isComputed(binding.value);
const tail = binding.value.type === 'MemberExpression' ? getTailSnippet(binding.value) : ''; const tail = binding.value.type === 'MemberExpression' ? getTailSnippet(binding.value) : '';
@ -167,20 +161,38 @@ export default class Component extends Node {
list[index]${tail} = childState.${binding.name}; list[index]${tail} = childState.${binding.name};
${binding.dependencies ${binding.dependencies
.map((prop: string) => `${newState}.${prop} = state.${prop};`) .map((name: string) => {
.join('\n')} const isStoreProp = generator.options.store && name[0] === '$';
`; const prop = isStoreProp ? name.slice(1) : name;
} const newState = isStoreProp ? 'newStoreState' : 'newState';
else if (binding.value.type === 'MemberExpression') { if (isStoreProp) hasStoreBindings = true;
setFromChild = deindent` else hasLocalBindings = true;
${binding.snippet} = childState.${binding.name};
${binding.dependencies.map((prop: string) => `${newState}.${prop} = state.${prop};`).join('\n')} return `${newState}.${prop} = state.${name};`;
})
.join('\n')}
`; `;
} }
else { else {
setFromChild = `${newState}.${key} = childState.${binding.name};`; const isStoreProp = generator.options.store && key[0] === '$';
const prop = isStoreProp ? key.slice(1) : key;
const newState = isStoreProp ? 'newStoreState' : 'newState';
if (isStoreProp) hasStoreBindings = true;
else hasLocalBindings = true;
if (binding.value.type === 'MemberExpression') {
setFromChild = deindent`
${binding.snippet} = childState.${binding.name};
${newState}.${prop} = state.${key};
`;
}
else {
setFromChild = `${newState}.${prop} = childState.${binding.name};`;
}
} }
statements.push(deindent` statements.push(deindent`
@ -190,16 +202,11 @@ export default class Component extends Node {
}` }`
); );
(isStoreProp ? setStoreFromChildOnChange : setParentFromChildOnChange).addConditional( builder.addConditional(
`!${name_updating}.${binding.name} && changed.${binding.name}`, `!${name_updating}.${binding.name} && changed.${binding.name}`,
setFromChild setFromChild
); );
(isStoreProp ? setStoreFromChildOnInit : setParentFromChildOnInit).addConditional(
`!${name_updating}.${binding.name}`,
setFromChild
);
// TODO could binding.dependencies.length ever be 0? // TODO could binding.dependencies.length ever be 0?
if (binding.dependencies.length) { if (binding.dependencies.length) {
updates.push(deindent` updates.push(deindent`
@ -215,44 +222,23 @@ export default class Component extends Node {
const initialisers = [ const initialisers = [
'state = #component.get()', 'state = #component.get()',
!setParentFromChildOnChange.isEmpty() && 'newState = {}', hasLocalBindings && 'newState = {}',
!setStoreFromChildOnChange.isEmpty() && 'newStoreState = {}', hasStoreBindings && 'newStoreState = {}',
].filter(Boolean).join(', '); ].filter(Boolean).join(', ');
componentInitProperties.push(deindent` componentInitProperties.push(deindent`
_bind: function(changed, childState) { _bind: function(changed, childState) {
var ${initialisers}; var ${initialisers};
${!setStoreFromChildOnChange.isEmpty() && deindent` ${builder}
${setStoreFromChildOnChange} ${hasStoreBindings && `#component.store.set(newStoreState);`}
${name_updating} = @assign({}, changed); ${hasLocalBindings && `#component._set(newState);`}
#component.store.set(newStoreState);
`}
${!setParentFromChildOnChange.isEmpty() && deindent`
${setParentFromChildOnChange}
${name_updating} = @assign({}, changed);
#component._set(newState);
`}
${name_updating} = {}; ${name_updating} = {};
} }
`); `);
// TODO can `!childState` ever be true?
beforecreate = deindent` beforecreate = deindent`
#component.root._beforecreate.push(function() { #component.root._beforecreate.push(function() {
var childState = ${name}.get(), ${initialisers}; ${name}._bind({ ${bindings.map(b => `${b.name}: 1`).join(', ')} }, ${name}.get());
if (!childState) return;
${setParentFromChildOnInit}
${!setStoreFromChildOnInit.isEmpty() && deindent`
${setStoreFromChildOnInit}
${name_updating} = { ${bindings.map((binding: Binding) => `${binding.name}: true`).join(', ')} };
#component.store.set(newStoreState);
`}
${!setParentFromChildOnInit.isEmpty() && deindent`
${setParentFromChildOnInit}
${name_updating} = { ${bindings.map((binding: Binding) => `${binding.name}: true`).join(', ')} };
#component._set(newState);
`}
${name_updating} = {};
}); });
`; `;
} else if (initialProps.length) { } else if (initialProps.length) {

@ -112,14 +112,14 @@ export class Parser {
throw new ParseError(message, this.template, index, this.filename); throw new ParseError(message, this.template, index, this.filename);
} }
eat(str: string, required?: boolean) { eat(str: string, required?: boolean, message?: string) {
if (this.match(str)) { if (this.match(str)) {
this.index += str.length; this.index += str.length;
return true; return true;
} }
if (required) { if (required) {
this.error(`Expected ${str}`); this.error(message || `Expected ${str}`);
} }
return false; return false;

@ -6,7 +6,7 @@ import { Parser } from '../index';
import { Node } from '../../interfaces'; import { Node } from '../../interfaces';
function trimWhitespace(block: Node, trimBefore: boolean, trimAfter: boolean) { function trimWhitespace(block: Node, trimBefore: boolean, trimAfter: boolean) {
if (!block.children) return; // AwaitBlock if (!block.children || block.children.length === 0) return; // AwaitBlock
const firstChild = block.children[0]; const firstChild = block.children[0];
const lastChild = block.children[block.children.length - 1]; const lastChild = block.children[block.children.length - 1];
@ -74,8 +74,6 @@ export default function mustache(parser: Parser) {
} }
// strip leading/trailing whitespace as necessary // strip leading/trailing whitespace as necessary
if (block.children && !block.children.length) parser.error(`Empty block`, block.start);
const charBefore = parser.template[block.start - 1]; const charBefore = parser.template[block.start - 1];
const charAfter = parser.template[parser.index]; const charAfter = parser.template[parser.index];
const trimBefore = !charBefore || whitespace.test(charBefore); const trimBefore = !charBefore || whitespace.test(charBefore);

@ -71,7 +71,7 @@ export default function tag(parser: Parser) {
if (parser.eat('!--')) { if (parser.eat('!--')) {
const data = parser.readUntil(/-->/); const data = parser.readUntil(/-->/);
parser.eat('-->'); parser.eat('-->', true, 'comment was left open, expected -->');
parser.current().children.push({ parser.current().children.push({
start, start,

@ -12,6 +12,13 @@ const meta = new Map([
[':Head', validateHead] [':Head', validateHead]
]); ]);
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));
}
export default function validateHtml(validator: Validator, html: Node) { export default function validateHtml(validator: Validator, html: Node) {
const refs = new Map(); const refs = new Map();
const refCallees: Node[] = []; const refCallees: Node[] = [];
@ -58,6 +65,10 @@ export default function validateHtml(validator: Validator, html: Node) {
} }
} }
if (validator.options.dev && isEmptyBlock(node)) {
validator.warn('Empty block', node.start);
}
if (node.children) { if (node.children) {
if (node.type === 'Element') elementStack.push(node); if (node.type === 'Element') elementStack.push(node);
stack.push(node); stack.push(node);

@ -93,7 +93,7 @@ export default function validate(
stylesheet: Stylesheet, stylesheet: Stylesheet,
options: CompileOptions options: CompileOptions
) { ) {
const { onwarn, onerror, name, filename, store } = options; const { onwarn, onerror, name, filename, store, dev } = options;
try { try {
if (name && !/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(name)) { if (name && !/^[a-zA-Z_$][a-zA-Z_$0-9]*$/.test(name)) {
@ -114,7 +114,8 @@ export default function validate(
onwarn, onwarn,
name, name,
filename, filename,
store store,
dev
}); });
if (parsed.js) { if (parsed.js) {

@ -0,0 +1,8 @@
{
"message": "comment was left open, expected -->",
"loc": {
"line": 1,
"column": 24
},
"pos": 24
}

@ -0,0 +1,42 @@
import { Store } from '../../../../store.js';
const store = new Store({
name: {
value: 'world'
}
});
export default {
store,
html: `
<h1>Hello world!</h1>
<input>
`,
test(assert, component, target, window) {
const input = target.querySelector('input');
const event = new window.Event('input');
const changeRecord = [];
store.onchange((state, changes) => {
changeRecord.push({ state, changes });
});
input.value = 'everybody';
input.dispatchEvent(event);
assert.equal(store.get('name').value, 'everybody');
assert.htmlEqual(target.innerHTML, `
<h1>Hello everybody!</h1>
<input>
`);
assert.deepEqual(changeRecord, [
{
state: { name: { value: 'everybody' } },
changes: { name: true }
}
]);
}
};

@ -0,0 +1,10 @@
<h1>Hello {{$name.value}}!</h1>
<TextInput bind:value=$name.value/>
<script>
import TextInput from './TextInput.html';
export default {
components: { TextInput }
};
</script>

@ -0,0 +1,30 @@
import { Store } from '../../../../store.js';
const store = new Store({
a: ['foo', 'bar', 'baz']
});
export default {
store,
html: `
<input><input><input>
<p>foo, bar, baz</p>
`,
test(assert, component, target, window) {
const event = new window.MouseEvent('input');
const inputs = target.querySelectorAll('input');
inputs[0].value = 'blah';
inputs[0].dispatchEvent(event);
assert.deepEqual(store.get('a'), ['blah', 'bar', 'baz']);
assert.htmlEqual(target.innerHTML, `
<input><input><input>
<p>blah, bar, baz</p>
`);
component.destroy();
},
};

@ -0,0 +1,15 @@
{{#each $a as x}}
<Widget bind:value='x'/>
{{/each}}
<p>{{$a.join(', ')}}</p>
<script>
import Widget from './Widget.html';
export default {
components: {
Widget
}
};
</script>

@ -16,6 +16,11 @@ export default {
const input = target.querySelector('input'); const input = target.querySelector('input');
const event = new window.Event('input'); const event = new window.Event('input');
const changeRecord = [];
store.onchange((state, changes) => {
changeRecord.push({ state, changes });
});
input.value = 'everybody'; input.value = 'everybody';
input.dispatchEvent(event); input.dispatchEvent(event);
@ -24,5 +29,12 @@ export default {
<h1>Hello everybody!</h1> <h1>Hello everybody!</h1>
<input> <input>
`); `);
assert.deepEqual(changeRecord, [
{
state: { name: 'everybody' },
changes: { name: true }
}
]);
} }
}; };

@ -1,6 +1,6 @@
import * as fs from "fs"; import * as fs from "fs";
import assert from "assert"; import assert from "assert";
import { svelte, tryToLoadJson } from "../helpers.js"; import { svelte, loadConfig, tryToLoadJson } from "../helpers.js";
describe("validate", () => { describe("validate", () => {
fs.readdirSync("test/validator/samples").forEach(dir => { fs.readdirSync("test/validator/samples").forEach(dir => {
@ -15,6 +15,7 @@ describe("validate", () => {
} }
(solo ? it.only : skip ? it.skip : it)(dir, () => { (solo ? it.only : skip ? it.skip : it)(dir, () => {
const config = loadConfig(`./validator/samples/${dir}/_config.js`);
const filename = `test/validator/samples/${dir}/input.html`; const filename = `test/validator/samples/${dir}/input.html`;
const input = fs.readFileSync(filename, "utf-8").replace(/\s+$/, ""); const input = fs.readFileSync(filename, "utf-8").replace(/\s+$/, "");
@ -32,7 +33,8 @@ describe("validate", () => {
pos: warning.pos, pos: warning.pos,
loc: warning.loc loc: warning.loc
}); });
} },
dev: config.dev
}); });
assert.deepEqual(warnings, expectedWarnings); assert.deepEqual(warnings, expectedWarnings);

@ -0,0 +1,10 @@
{{#each things as thing}}
<span>this only exists...</span>
<span>...to increase test coverage</span>
{{/each}}
{{#each things as thing}}{{soDoesThis}}{{/each}}
{{#each things as thing}}
andThis
{{/each}}

@ -0,0 +1,3 @@
export default {
dev: true
};

@ -0,0 +1,5 @@
{{#each things as thing}}
{{/each}}
{{#each things as thing}}{{/each}}

@ -0,0 +1,18 @@
[
{
"message": "Empty block",
"loc": {
"line": 1,
"column": 0
},
"pos": 0
},
{
"message": "Empty block",
"loc": {
"line": 5,
"column": 0
},
"pos": 38
}
]

@ -0,0 +1,5 @@
{{#each things as thing}}
{{/each}}
{{#each things as thing}}{{/each}}
Loading…
Cancel
Save