Merge branch 'master' into gh-697

pull/729/head
Rich Harris 8 years ago
commit 333568275b

@ -212,7 +212,7 @@ export default function visitComponent(
${updates.join('\n')}
if ( Object.keys( ${name}_changes ).length ) ${name}.set( ${name}_changes );
if ( Object.keys( ${name}_changes ).length ) ${name}._set( ${name}_changes );
`);
}

@ -13,22 +13,25 @@ export default function visitEventHandler(
local
) {
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.addSourcemapLocations(attribute.expression);
generator.code.prependRight(
attribute.expression.start,
`${block.alias('component')}.`
);
const usedContexts: string[] = [];
attribute.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, null, true);
contexts.forEach(context => {
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
if (!~local.allUsedContexts.indexOf(context))
local.allUsedContexts.push(context);
if (attribute.expression) {
generator.addSourcemapLocations(attribute.expression);
generator.code.prependRight(
attribute.expression.start,
`${block.alias('component')}.`
);
attribute.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, null, true);
contexts.forEach(context => {
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
if (!~local.allUsedContexts.indexOf(context))
local.allUsedContexts.push(context);
});
});
});
}
// TODO hoist event handlers? can do `this.__component.method(...)`
const declarations = usedContexts.map(name => {
@ -42,7 +45,9 @@ export default function visitEventHandler(
const handlerBody =
(declarations.length ? declarations.join('\n') + '\n\n' : '') +
`[✂${attribute.expression.start}-${attribute.expression.end}✂];`;
(attribute.expression ?
`[✂${attribute.expression.start}-${attribute.expression.end}✂];` :
`${block.alias('component')}.fire('${attribute.name}', event);`);
local.create.addBlock(deindent`
${local.name}.on( '${attribute.name}', function ( event ) {

@ -16,30 +16,33 @@ export default function visitEventHandler(
const isCustomEvent = generator.events.has(name);
const shouldHoist = !isCustomEvent && state.inEachBlock;
generator.addSourcemapLocations(attribute.expression);
const flattened = flattenReference(attribute.expression.callee);
if (flattened.name !== 'event' && flattened.name !== 'this') {
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.code.prependRight(
attribute.expression.start,
`${block.alias('component')}.`
);
if (shouldHoist) state.usesComponent = true; // this feels a bit hacky but it works!
}
const context = shouldHoist ? null : state.parentNode;
const usedContexts: string[] = [];
attribute.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, context, true);
contexts.forEach(context => {
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
if (!~state.allUsedContexts.indexOf(context))
state.allUsedContexts.push(context);
if (attribute.expression) {
generator.addSourcemapLocations(attribute.expression);
const flattened = flattenReference(attribute.expression.callee);
if (flattened.name !== 'event' && flattened.name !== 'this') {
// allow event.stopPropagation(), this.select() etc
// TODO verify that it's a valid callee (i.e. built-in or declared method)
generator.code.prependRight(
attribute.expression.start,
`${block.alias('component')}.`
);
if (shouldHoist) state.usesComponent = true; // this feels a bit hacky but it works!
}
attribute.expression.arguments.forEach((arg: Node) => {
const { contexts } = block.contextualise(arg, context, true);
contexts.forEach(context => {
if (!~usedContexts.indexOf(context)) usedContexts.push(context);
if (!~state.allUsedContexts.indexOf(context))
state.allUsedContexts.push(context);
});
});
});
}
const _this = context || 'this';
const declarations = usedContexts.map(name => {
@ -66,7 +69,9 @@ export default function visitEventHandler(
${state.usesComponent &&
`var ${block.alias('component')} = ${_this}._svelte.component;`}
${declarations}
[${attribute.expression.start}-${attribute.expression.end}];
${attribute.expression ?
`[✂${attribute.expression.start}-${attribute.expression.end}✂];` :
`${block.alias('component')}.fire('${attribute.name}', event);`}
`;
if (isCustomEvent) {

@ -17,7 +17,7 @@ export default function visitRef(
`#component.refs.${name} = ${state.parentNode};`
);
block.builders.unmount.addLine(deindent`
block.builders.destroy.addLine(deindent`
if ( #component.refs.${name} === ${state.parentNode} ) #component.refs.${name} = null;
`);

@ -111,6 +111,8 @@ export class Parser {
if (required) {
this.error(`Expected ${str}`);
}
return false;
}
match(str: string) {

@ -43,16 +43,21 @@ function readExpression(parser: Parser, start: number, quoteMark) {
export function readEventHandlerDirective(
parser: Parser,
start: number,
name: string
name: string,
hasValue: boolean
) {
const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
let expression;
const expressionStart = parser.index;
if (hasValue) {
const quoteMark = parser.eat(`'`) ? `'` : parser.eat(`"`) ? `"` : null;
const expression = readExpression(parser, expressionStart, quoteMark);
const expressionStart = parser.index;
if (expression.type !== 'CallExpression') {
parser.error(`Expected call expression`, expressionStart);
expression = readExpression(parser, expressionStart, quoteMark);
if (expression.type !== 'CallExpression') {
parser.error(`Expected call expression`, expressionStart);
}
}
return {

@ -256,8 +256,7 @@ function readAttribute(parser: Parser, uniqueNames) {
parser.allowWhitespace();
if (/^on:/.test(name)) {
parser.eat('=', true);
return readEventHandlerDirective(parser, start, name.slice(3));
return readEventHandlerDirective(parser, start, name.slice(3), parser.eat('='));
}
if (/^bind:/.test(name)) {

@ -1,6 +1,8 @@
import * as namespaces from '../../utils/namespaces';
import validateElement from './validateElement';
import validateWindow from './validateWindow';
import fuzzymatch from '../utils/fuzzymatch'
import flattenReference from '../../utils/flattenReference';
import { Validator } from '../index';
import { Node } from '../../interfaces';
@ -11,6 +13,9 @@ const meta = new Map([[':Window', validateWindow]]);
export default function validateHtml(validator: Validator, html: Node) {
let elementDepth = 0;
const refs = new Map();
const refCallees: Node[] = [];
function visit(node: Node) {
if (node.type === 'Element') {
if (
@ -25,12 +30,12 @@ export default function validateHtml(validator: Validator, html: Node) {
}
if (meta.has(node.name)) {
return meta.get(node.name)(validator, node);
return meta.get(node.name)(validator, node, refs, refCallees);
}
elementDepth += 1;
validateElement(validator, node);
validateElement(validator, node, refs, refCallees);
} else if (node.type === 'EachBlock') {
if (validator.helpers.has(node.context)) {
let c = node.expression.end;
@ -61,4 +66,20 @@ export default function validateHtml(validator: Validator, html: Node) {
}
html.children.forEach(visit);
refCallees.forEach(callee => {
const { parts } = flattenReference(callee);
const ref = parts[1];
if (refs.has(ref)) {
// TODO check method is valid, e.g. `audio.stop()` should be `audio.pause()`
} else {
const match = fuzzymatch(ref, Array.from(refs.keys()));
let message = `'refs.${ref}' does not exist`;
if (match) message += ` (did you mean 'refs.${match}'?)`;
validator.error(message, callee.start);
}
});
}

@ -2,7 +2,7 @@ import validateEventHandler from './validateEventHandler';
import { Validator } from '../index';
import { Node } from '../../interfaces';
export default function validateElement(validator: Validator, node: Node) {
export default function validateElement(validator: Validator, node: Node, refs: Map<string, Node[]>, refCallees: Node[]) {
const isComponent =
node.name === ':Self' || validator.components.has(node.name);
@ -16,6 +16,11 @@ export default function validateElement(validator: Validator, node: Node) {
let hasTransition: boolean;
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Ref') {
if (!refs.has(attribute.name)) refs.set(attribute.name, []);
refs.get(attribute.name).push(node);
}
if (!isComponent && attribute.type === 'Binding') {
const { name } = attribute;
@ -80,7 +85,7 @@ export default function validateElement(validator: Validator, node: Node) {
);
}
} else if (attribute.type === 'EventHandler') {
validateEventHandler(validator, attribute);
validateEventHandler(validator, attribute, refCallees);
} else if (attribute.type === 'Transition') {
const bidi = attribute.intro && attribute.outro;

@ -7,8 +7,11 @@ const validBuiltins = new Set(['set', 'fire', 'destroy']);
export default function validateEventHandlerCallee(
validator: Validator,
attribute: Node
attribute: Node,
refCallees: Node[]
) {
if (!attribute.expression) return;
const { callee, start, type } = attribute.expression;
if (type !== 'CallExpression') {
@ -18,6 +21,12 @@ export default function validateEventHandlerCallee(
const { name } = flattenReference(callee);
if (name === 'this' || name === 'event') return;
if (name === 'refs') {
refCallees.push(callee);
return;
}
if (
(callee.type === 'Identifier' && validBuiltins.has(callee.name)) ||
validator.methods.has(callee.name)

@ -14,7 +14,7 @@ const validBindings = [
'scrollY',
];
export default function validateWindow(validator: Validator, node: Node) {
export default function validateWindow(validator: Validator, node: Node, refs: Map<string, Node[]>, refCallees: Node[]) {
node.attributes.forEach((attribute: Node) => {
if (attribute.type === 'Binding') {
if (attribute.value.type !== 'Identifier') {
@ -50,7 +50,7 @@ export default function validateWindow(validator: Validator, node: Node) {
}
}
} else if (attribute.type === 'EventHandler') {
validateEventHandler(validator, attribute);
validateEventHandler(validator, attribute, refCallees);
}
});
}

@ -0,0 +1,18 @@
<li>
{{yield}}
</li>
<script>
const initialValues = {
'id-0': 'zero',
'id-1': 'one',
'id-2': 'two',
'id-3': 'three'
};
export default {
oncreate() {
this.set({ value: initialValues[this.get('id')] });
}
};
</script>

@ -0,0 +1,33 @@
export default {
'skip-ssr': true,
data: {
count: 3
},
html: `
<input type='number'>
<ol>
<li>id-2: value is two</li>
<li>id-1: value is one</li>
<li>id-0: value is zero</li>
</ol>
`,
test (assert, component, target, window) {
const input = target.querySelector('input');
input.value = 4;
input.dispatchEvent(new window.Event('input'));
assert.htmlEqual(target.innerHTML, `
<input type='number'>
<ol>
<li>id-3: value is three</li>
<li>id-2: value is two</li>
<li>id-1: value is one</li>
<li>id-0: value is zero</li>
</ol>
`);
}
};

@ -0,0 +1,34 @@
<input type='number' bind:value='count'>
<ol>
{{#each ids as object @id}}
<Nested bind:value='idToValue[object.id]' id='{{object.id}}'>
{{object.id}}: value is {{idToValue[object.id]}}
</Nested>
{{/each}}
</ol>
<script>
import Nested from './Nested.html';
export default {
data() {
return {
idToValue: Object.create(null)
};
},
computed: {
ids(count) {
return new Array(count)
.fill(null)
.map((_, i) => ({ id: 'id-' + i}))
.reverse();
}
},
components: {
Nested
}
};
</script>

@ -0,0 +1 @@
<button on:click='fire("foo", { answer: 42 })'>click me</button>

@ -0,0 +1,18 @@
export default {
html: `
<button>click me</button>
`,
test (assert, component, target, window) {
const button = target.querySelector('button');
const event = new window.MouseEvent('click');
let answer;
component.on('foo', event => {
answer = event.answer;
});
button.dispatchEvent(event);
assert.equal(answer, 42);
}
};

@ -0,0 +1,11 @@
<Widget on:foo/>
<script>
import Widget from './Widget.html';
export default {
components: {
Widget
}
};
</script>

@ -0,0 +1,13 @@
export default {
html: `
<button>click me</button>
`,
test (assert, component, target, window) {
const button = target.querySelector('button');
const event = new window.MouseEvent('click');
button.dispatchEvent(event);
assert.ok(component.clicked);
}
};

@ -0,0 +1,11 @@
<button on:click>click me</button>
<script>
export default {
oncreate () {
this.on('click', () => {
this.clicked = true;
});
}
};
</script>

@ -0,0 +1,9 @@
<div ref:element></div>
<script>
export default {
ondestroy() {
this.refOnDestroy = this.refs.element;
}
};
</script>

@ -0,0 +1,9 @@
export default {
test(assert, component, target) {
const top = component.refs.top;
const div = target.querySelector('div');
component.set({ visible: false });
assert.equal(top.refOnDestroy, div);
}
};

@ -0,0 +1,18 @@
{{#if visible}}
<Top ref:top></Top>
{{/if}}
<script>
import Top from './Top.html';
export default {
data() {
return {
visible: true
};
},
components: {
Top
}
};
</script>

@ -17,6 +17,10 @@ describe("validate", () => {
const filename = `test/validator/samples/${dir}/input.html`;
const input = fs.readFileSync(filename, "utf-8").replace(/\s+$/, "");
const expectedWarnings = tryToLoadJson(`test/validator/samples/${dir}/warnings.json`) || [];
const expectedErrors = tryToLoadJson(`test/validator/samples/${dir}/errors.json`);
let error;
try {
const warnings = [];
@ -30,20 +34,25 @@ describe("validate", () => {
}
});
const expectedWarnings =
tryToLoadJson(`test/validator/samples/${dir}/warnings.json`) || [];
assert.deepEqual(warnings, expectedWarnings);
} catch (err) {
try {
const expected = require(`./samples/${dir}/errors.json`)[0];
} catch (e) {
error = e;
}
const expected = expectedErrors && expectedErrors[0];
assert.equal(err.message, expected.message);
assert.deepEqual(err.loc, expected.loc);
assert.equal(err.pos, expected.pos);
} catch (err2) {
throw err2.code === "MODULE_NOT_FOUND" ? err : err2;
if (error || expected) {
if (error && !expected) {
throw error;
}
if (expected && !error) {
throw new Error(`Expected an error: ${expected.message}`);
}
assert.equal(error.message, expected.message);
assert.deepEqual(error.loc, expected.loc);
assert.equal(error.pos, expected.pos);
}
});
});

@ -0,0 +1,8 @@
[{
"message": "'refs.inputx' does not exist (did you mean 'refs.input'?)",
"pos": 36,
"loc": {
"line": 2,
"column": 18
}
}]

@ -0,0 +1,2 @@
<input ref:input>
<button on:click='refs.inputx.focus()'>focus input</button>

@ -0,0 +1,2 @@
<input ref:input>
<button on:click='refs.input.focus()'>focus input</button>
Loading…
Cancel
Save