Repository: sveltejs/svelte (JavaScript) — Bug Fix

Issue:
Media bindings broken in v3
https://v3.svelte.technology/repl?version=3.0.0-alpha8&demo=binding-media-elements


Discussion:

Resolving patch:
diff --git a/src/compile/nodes/Binding.ts b/src/compile/nodes/Binding.ts
--- a/src/compile/nodes/Binding.ts
+++ b/src/compile/nodes/Binding.ts
@@ -7,7 +7,6 @@ export default class Binding extends Node {
 	name: string;
 	expression: Expression;
 	isContextual: boolean;
-	usesContext: boolean;
 	obj: string;
 	prop: string;
 
@@ -34,13 +33,9 @@ export default class Binding extends Node {
 			prop = `[✂${this.expression.node.property.start}-${this.expression.node.property.end}✂]`;
 			if (!this.expression.node.computed) prop = `'${prop}'`;
 			obj = `[✂${this.expression.node.object.start}-${this.expression.node.object.end}✂]`;
-
-			this.usesContext = true;
 		} else {
 			obj = 'ctx';
 			prop = `'${name}'`;
-
-			this.usesContext = scope.names.has(name);
 		}
 
 		this.obj = obj;
diff --git a/src/compile/render-dom/Block.ts b/src/compile/render-dom/Block.ts
--- a/src/compile/render-dom/Block.ts
+++ b/src/compile/render-dom/Block.ts
@@ -29,7 +29,7 @@ export default class Block {
 
 	dependencies: Set<string>;
 
-	bindings: Map<string, () => { object: string, property: string, snippet: string }>;
+	bindings: Map<string, { object: string, property: string, snippet: string }>;
 	contextOwners: Map<string, EachBlockWrapper>;
 
 	builders: {
diff --git a/src/compile/render-dom/wrappers/EachBlock.ts b/src/compile/render-dom/wrappers/EachBlock.ts
--- a/src/compile/render-dom/wrappers/EachBlock.ts
+++ b/src/compile/render-dom/wrappers/EachBlock.ts
@@ -97,15 +97,33 @@ export default class EachBlockWrapper extends Wrapper {
 
 		this.indexName = this.node.index || renderer.component.getUniqueName(`${this.node.context}_index`);
 
+		// hack the sourcemap, so that if data is missing the bug
+		// is easy to find
+		let c = this.node.start + 2;
+		while (renderer.component.source[c] !== 'e') c += 1;
+		renderer.component.code.overwrite(c, c + 4, 'length');
+		const length = `[✂${c}-${c+4}✂]`;
+
+		this.vars = {
+			create_each_block: this.block.name,
+			each_block_value: renderer.component.getUniqueName(`${this.var}_value`),
+			get_each_context: renderer.component.getUniqueName(`get_${this.var}_context`),
+			iterations: block.getUniqueName(`${this.var}_blocks`),
+			length: `[✂${c}-${c+4}✂]`,
+
+			// filled out later
+			anchor: null,
+			mountOrIntro: null
+		};
+
 		node.contexts.forEach(prop => {
 			this.block.contextOwners.set(prop.key.name, this);
 
-			// TODO this doesn't feel great
-			this.block.bindings.set(prop.key.name, () => ({
+			this.block.bindings.set(prop.key.name, {
 				object: this.vars.each_block_value,
 				property: this.indexName,
 				snippet: `${this.vars.each_block_value}[${this.indexName}]${prop.tail}`
-			}));
+			});
 		});
 
 		if (this.node.index) {
@@ -147,30 +165,15 @@ export default class EachBlockWrapper extends Wrapper {
 		const { renderer } = this;
 		const { component } = renderer;
 
-		// hack the sourcemap, so that if data is missing the bug
-		// is easy to find
-		let c = this.node.start + 2;
-		while (component.source[c] !== 'e') c += 1;
-		component.code.overwrite(c, c + 4, 'length');
-		const length = `[✂${c}-${c+4}✂]`;
-
 		const needsAnchor = this.next
 			? !this.next.isDomNode() :
 			!parentNode || !this.parent.isDomNode();
 
-		this.vars = {
-			anchor: needsAnchor
-				? block.getUniqueName(`${this.var}_anchor`)
-				: (this.next && this.next.var) || 'null',
-			create_each_block: this.block.name,
-			each_block_value: renderer.component.getUniqueName(`${this.var}_value`),
-			get_each_context: renderer.component.getUniqueName(`get_${this.var}_context`),
-			iterations: block.getUniqueName(`${this.var}_blocks`),
-			length: `[✂${c}-${c+4}✂]`,
-			mountOrIntro: (this.block.hasIntroMethod || this.block.hasOutroMethod)
-				? 'i'
-				: 'm'
-		};
+		this.vars.anchor = needsAnchor
+			? block.getUniqueName(`${this.var}_anchor`)
+			: (this.next && this.next.var) || 'null';
+
+		this.vars.mountOrIntro = (this.block.hasIntroMethod || this.block.hasOutroMethod) ? 'i' : 'm';
 
 		this.contextProps = this.node.contexts.map(prop => `child_ctx.${prop.key.name} = list[i]${prop.tail};`);
 
@@ -212,7 +215,7 @@ export default class EachBlockWrapper extends Wrapper {
 
 			// TODO neaten this up... will end up with an empty line in the block
 			block.builders.init.addBlock(deindent`
-				if (!${this.vars.each_block_value}.${length}) {
+				if (!${this.vars.each_block_value}.${this.vars.length}) {
 					${each_block_else} = ${this.else.block.name}(#component, ctx);
 					${each_block_else}.c();
 				}
@@ -228,9 +231,9 @@ export default class EachBlockWrapper extends Wrapper {
 
 			if (this.else.block.hasUpdateMethod) {
 				block.builders.update.addBlock(deindent`
-					if (!${this.vars.each_block_value}.${length} && ${each_block_else}) {
+					if (!${this.vars.each_block_value}.${this.vars.length} && ${each_block_else}) {
 						${each_block_else}.p(changed, ctx);
-					} else if (!${this.vars.each_block_value}.${length}) {
+					} else if (!${this.vars.each_block_value}.${this.vars.length}) {
 						${each_block_else} = ${this.else.block.name}(#component, ctx);
 						${each_block_else}.c();
 						${each_block_else}.${mountOrIntro}(${initialMountNode}, ${this.vars.anchor});
@@ -241,7 +244,7 @@ export default class EachBlockWrapper extends Wrapper {
 				`);
 			} else {
 				block.builders.update.addBlock(deindent`
-					if (${this.vars.each_block_value}.${length}) {
+					if (${this.vars.each_block_value}.${this.vars.length}) {
 						if (${each_block_else}) {
 							${each_block_else}.d(1);
 							${each_block_else} = null;
diff --git a/src/compile/render-dom/wrappers/Element/Binding.ts b/src/compile/render-dom/wrappers/Element/Binding.ts
--- a/src/compile/render-dom/wrappers/Element/Binding.ts
+++ b/src/compile/render-dom/wrappers/Element/Binding.ts
@@ -21,11 +21,15 @@ export default class BindingWrapper {
 	parent: ElementWrapper;
 
 	object: string;
-	handler: any; // TODO
-	updateDom: string;
+	handler: {
+		usesContext: boolean;
+		mutation: string;
+		contextual_dependencies: Set<string>
+	};
+	snippet: string;
 	initialUpdate: string;
+	isReadOnly: boolean;
 	needsLock: boolean;
-	updateCondition: string;
 
 	constructor(block: Block, node: Binding, parent: ElementWrapper) {
 		this.node = node;
@@ -51,45 +55,34 @@ export default class BindingWrapper {
 
 			eachBlock.hasBinding = true;
 		}
-	}
 
-	isReadOnlyMediaAttribute() {
-		return readOnlyMediaAttributes.has(this.node.name);
-	}
-
-	munge(block: Block) {
-		const { parent } = this;
-		const { renderer } = parent;
+		this.object = getObject(this.node.expression.node).name;
 
-		const needsLock = (
-			parent.node.name !== 'input' ||
-			!/radio|checkbox|range|color/.test(parent.node.getStaticAttributeValue('type'))
-		);
+		// TODO unfortunate code is necessary because we need to use `ctx`
+		// inside the fragment, but not inside the <script>
+		const contextless_snippet = this.parent.renderer.component.source.slice(this.node.expression.node.start, this.node.expression.node.end);
 
-		const isReadOnly = (
-			(parent.node.isMediaNode() && readOnlyMediaAttributes.has(this.node.name)) ||
-			dimensions.test(this.node.name)
-		);
+		// view to model
+		this.handler = getEventHandler(this, parent.renderer, block, this.object, contextless_snippet);
 
-		let updateConditions: string[] = [];
+		this.snippet = this.node.expression.render();
 
-		const { name } = getObject(this.node.expression.node);
+		const type = parent.node.getStaticAttributeValue('type');
 
-		const snippet = this.node.expression.render();
+		this.isReadOnly = (
+			dimensions.test(this.node.name) ||
+			(parent.node.isMediaNode() && readOnlyMediaAttributes.has(this.node.name)) ||
+			(parent.node.name === 'input' && type === 'file') // TODO others?
+		);
 
-		// TODO unfortunate code is necessary because we need to use `ctx`
-		// inside the fragment, but not inside the <script>
-		const contextless_snippet = this.parent.renderer.component.source.slice(this.node.expression.node.start, this.node.expression.node.end);
+		this.needsLock = this.node.name === 'currentTime'; // TODO others?
+	}
 
-		// special case: if you have e.g. `<input type=checkbox bind:checked=selected.done>`
-		// and `selected` is an object chosen with a <select>, then when `checked` changes,
-		// we need to tell the component to update all the values `selected` might be
-		// pointing to
-		// TODO should this happen in preprocess?
+	get_dependencies() {
 		const dependencies = new Set(this.node.expression.dependencies);
 
 		this.node.expression.dependencies.forEach((prop: string) => {
-			const indirectDependencies = renderer.component.indirectDependencies.get(prop);
+			const indirectDependencies = this.parent.renderer.component.indirectDependencies.get(prop);
 			if (indirectDependencies) {
 				indirectDependencies.forEach(indirectDependency => {
 					dependencies.add(indirectDependency);
@@ -97,48 +90,19 @@ export default class BindingWrapper {
 			}
 		});
 
-		// view to model
-		const valueFromDom = getValueFromDom(renderer, this.parent, this);
-		const handler = getEventHandler(this, renderer, block, name, contextless_snippet, valueFromDom);
-
-		// model to view
-		let updateDom = getDomUpdater(parent, this, snippet);
-		let initialUpdate = updateDom;
-
-		// special cases
-		if (this.node.name === 'group') {
-			const bindingGroup = getBindingGroup(renderer, this.node.expression.node);
-
-			block.builders.hydrate.addLine(
-				`(#component.$$.binding_groups[${bindingGroup}] || (#component.$$.binding_groups[${bindingGroup}] = [])).push(${parent.var});`
-			);
-
-			block.builders.destroy.addLine(
-				`#component.$$.binding_groups[${bindingGroup}].splice(#component.$$.binding_groups[${bindingGroup}].indexOf(${parent.var}), 1);`
-			);
-		}
-
-		if (this.node.name === 'currentTime' || this.node.name === 'volume') {
-			updateConditions.push(`!isNaN(${snippet})`);
+		return dependencies;
+	}
 
-			if (this.node.name === 'currentTime') initialUpdate = null;
-		}
+	isReadOnlyMediaAttribute() {
+		return readOnlyMediaAttributes.has(this.node.name);
+	}
 
-		if (this.node.name === 'paused') {
-			// this is necessary to prevent audio restarting by itself
-			const last = block.getUniqueName(`${parent.var}_is_paused`);
-			block.addVariable(last, 'true');
+	render(block: Block, lock: string) {
+		if (this.isReadOnly) return;
 
-			updateConditions.push(`${last} !== (${last} = ${snippet})`);
-			updateDom = `${parent.var}[${last} ? "pause" : "play"]();`;
-			initialUpdate = null;
-		}
+		const { parent } = this;
 
-		// bind:offsetWidth and bind:offsetHeight
-		if (dimensions.test(this.node.name)) {
-			initialUpdate = null;
-			updateDom = null;
-		}
+		let updateConditions: string[] = this.needsLock ? [`!${lock}`] : [];
 
 		const dependencyArray = [...this.node.expression.dynamic_dependencies]
 
@@ -150,27 +114,58 @@ export default class BindingWrapper {
 			)
 		}
 
-		return {
-			name: this.node.name,
-			object: name,
-			handler,
-			snippet,
-			usesContext: handler.usesContext,
-			updateDom: updateDom,
-			initialUpdate: initialUpdate,
-			needsLock: !isReadOnly && needsLock,
-			updateCondition: updateConditions.length ? updateConditions.join(' && ') : undefined,
-			isReadOnlyMediaAttribute: this.isReadOnlyMediaAttribute(),
-			dependencies,
-			contextual_dependencies: this.node.expression.contextual_dependencies
-		};
+		// model to view
+		let updateDom = getDomUpdater(parent, this);
+
+		// special cases
+		switch (this.node.name) {
+			case 'group':
+				const bindingGroup = getBindingGroup(parent.renderer, this.node.expression.node);
+
+				block.builders.hydrate.addLine(
+					`(#component.$$.binding_groups[${bindingGroup}] || (#component.$$.binding_groups[${bindingGroup}] = [])).push(${parent.var});`
+				);
+
+				block.builders.destroy.addLine(
+					`#component.$$.binding_groups[${bindingGroup}].splice(#component.$$.binding_groups[${bindingGroup}].indexOf(${parent.var}), 1);`
+				);
+				break;
+
+			case 'currentTime':
+			case 'volume':
+				updateConditions.push(`!isNaN(${this.snippet})`);
+				break;
+
+			case 'paused':
+				// this is necessary to prevent audio restarting by itself
+				const last = block.getUniqueName(`${parent.var}_is_paused`);
+				block.addVariable(last, 'true');
+
+				updateConditions.push(`${last} !== (${last} = ${this.snippet})`);
+				updateDom = `${parent.var}[${last} ? "pause" : "play"]();`;
+				break;
+
+			case 'value':
+				if (parent.getStaticAttributeValue('type') === 'file') {
+					updateDom = null;
+				}
+		}
+
+		if (updateDom) {
+			block.builders.update.addLine(
+				updateConditions.length ? `if (${updateConditions.join(' && ')}) ${updateDom}` : updateDom
+			);
+		}
+
+		if (!/(currentTime|paused)/.test(this.node.name)) {
+			block.builders.mount.addBlock(updateDom);
+		}
 	}
 }
 
 function getDomUpdater(
 	element: ElementWrapper,
-	binding: BindingWrapper,
-	snippet: string
+	binding: BindingWrapper
 ) {
 	const { node } = element;
 
@@ -184,21 +179,21 @@ function getDomUpdater(
 
 	if (node.name === 'select') {
 		return node.getStaticAttributeValue('multiple') === true ?
-			`@selectOptions(${element.var}, ${snippet})` :
-			`@selectOption(${element.var}, ${snippet})`;
+			`@selectOptions(${element.var}, ${binding.snippet})` :
+			`@selectOption(${element.var}, ${binding.snippet})`;
 	}
 
 	if (binding.node.name === 'group') {
 		const type = node.getStaticAttributeValue('type');
 
 		const condition = type === 'checkbox'
-			? `~${snippet}.indexOf(${element.var}.__value)`
-			: `${element.var}.__value === ${snippet}`;
+			? `~${binding.snippet}.indexOf(${element.var}.__value)`
+			: `${element.var}.__value === ${binding.snippet}`;
 
 		return `${element.var}.checked = ${condition};`
 	}
 
-	return `${element.var}.${binding.node.name} = ${snippet};`;
+	return `${element.var}.${binding.node.name} = ${binding.snippet};`;
 }
 
 function getBindingGroup(renderer: Renderer, value: Node) {
@@ -221,9 +216,10 @@ function getEventHandler(
 	renderer: Renderer,
 	block: Block,
 	name: string,
-	snippet: string,
-	value: string
+	snippet: string
 ) {
+	const value = getValueFromDom(renderer, binding.parent, binding);
+
 	if (binding.node.isContextual) {
 		let tail = '';
 		if (binding.node.expression.node.type === 'MemberExpression') {
@@ -231,7 +227,7 @@ function getEventHandler(
 			tail = renderer.component.source.slice(start, end);
 		}
 
-		const { object, property, snippet } = block.bindings.get(name)();
+		const { object, property, snippet } = block.bindings.get(name);
 
 		return {
 			usesContext: true,
diff --git a/src/compile/render-dom/wrappers/Element/index.ts b/src/compile/render-dom/wrappers/Element/index.ts
--- a/src/compile/render-dom/wrappers/Element/index.ts
+++ b/src/compile/render-dom/wrappers/Element/index.ts
@@ -90,7 +90,6 @@ export default class ElementWrapper extends Wrapper {
 	attributes: AttributeWrapper[];
 	bindings: Binding[];
 	classDependencies: string[];
-	initialUpdate: string;
 
 	slotOwner?: InlineComponentWrapper;
 	selectBindingDependencies?: Set<string>;
@@ -137,25 +136,11 @@ export default class ElementWrapper extends Wrapper {
 			return new AttributeWrapper(this, block, attribute);
 		});
 
-		let has_bindings;
-		const binding_lookup = {};
-		this.node.bindings.forEach(binding => {
-			binding_lookup[binding.name] = binding;
-			has_bindings = true;
-		});
-
-		const type = this.node.getStaticAttributeValue('type');
-
 		// ordinarily, there'll only be one... but we need to handle
 		// the rare case where an element can have multiple bindings,
 		// e.g. <audio bind:paused bind:currentTime>
 		this.bindings = this.node.bindings.map(binding => new Binding(block, binding, this));
 
-		// TODO remove this, it's just useful during refactoring
-		if (has_bindings && !this.bindings.length) {
-			throw new Error(`no binding was created`);
-		}
-
 		if (node.intro || node.outro) {
 			if (node.intro) block.addIntro();
 			if (node.outro) block.addOutro();
@@ -305,10 +290,6 @@ export default class ElementWrapper extends Wrapper {
 		this.addActions(block);
 		this.addClasses(block);
 
-		if (this.initialUpdate) {
-			block.builders.mount.addBlock(this.initialUpdate);
-		}
-
 		if (nodes && this.renderer.options.hydratable) {
 			block.builders.claim.addLine(
 				`${nodes}.forEach(@detachNode);`
@@ -389,12 +370,7 @@ export default class ElementWrapper extends Wrapper {
 
 		renderer.component.has_reactive_assignments = true;
 
-		const needsLock = this.node.name !== 'input' || !/radio|checkbox|range|color/.test(this.getStaticAttributeValue('type'));
-
-		// TODO munge in constructor
-		const mungedBindings = this.bindings.map(binding => binding.munge(block));
-
-		const lock = mungedBindings.some(binding => binding.needsLock) ?
+		const lock = this.bindings.some(binding => binding.needsLock) ?
 			block.getUniqueName(`${this.var}_updating`) :
 			null;
 
@@ -403,9 +379,9 @@ export default class ElementWrapper extends Wrapper {
 		const groups = events
 			.map(event => ({
 				events: event.eventNames,
-				bindings: mungedBindings
-					.filter(binding => binding.name !== 'this')
-					.filter(binding => event.filter(this.node, binding.name))
+				bindings: this.bindings
+					.filter(binding => binding.node.name !== 'this')
+					.filter(binding => event.filter(this.node, binding.node.name))
 			}))
 			.filter(group => group.bindings.length);
 
@@ -414,6 +390,7 @@ export default class ElementWrapper extends Wrapper {
 			renderer.component.declarations.push(handler);
 			renderer.component.template_references.add(handler);
 
+			// TODO figure out how to handle locks
 			const needsLock = group.bindings.some(binding => binding.needsLock);
 
 			const dependencies = new Set();
@@ -421,22 +398,13 @@ export default class ElementWrapper extends Wrapper {
 
 			group.bindings.forEach(binding => {
 				// TODO this is a mess
-				addToSet(dependencies, binding.dependencies);
-				addToSet(contextual_dependencies, binding.contextual_dependencies);
+				addToSet(dependencies, binding.get_dependencies());
+				addToSet(contextual_dependencies, binding.node.expression.contextual_dependencies);
 				addToSet(contextual_dependencies, binding.handler.contextual_dependencies);
 
-				if (!binding.updateDom) return;
-
-				const updateConditions = needsLock ? [`!${lock}`] : [];
-				if (binding.updateCondition) updateConditions.push(binding.updateCondition);
-
-				block.builders.update.addLine(
-					updateConditions.length ? `if (${updateConditions.join(' && ')}) ${binding.updateDom}` : binding.updateDom
-				);
+				binding.render(block, lock);
 			});
 
-			const mutations = group.bindings.map(binding => binding.handler.mutation).filter(Boolean).join('\n');
-
 			// media bindings — awkward special case. The native timeupdate events
 			// fire too infrequently, so we need to take matters into our
 			// own hands
@@ -446,49 +414,35 @@ export default class ElementWrapper extends Wrapper {
 				block.addVariable(animation_frame);
 			}
 
-			// TODO figure out how to handle locks
+			const has_local_function = contextual_dependencies.size > 0 || needsLock || animation_frame;
 
 			let callee;
 
 			// TODO dry this out — similar code for event handlers and component bindings
-			if (contextual_dependencies.size > 0) {
-				const deps = Array.from(contextual_dependencies);
-
+			if (has_local_function) {
+				// need to create a block-local function that calls an instance-level function
 				block.builders.init.addBlock(deindent`
 					function ${handler}() {
-						ctx.${handler}.call(this, ctx);
-					}
-				`);
-
-				this.renderer.component.partly_hoisted.push(deindent`
-					function ${handler}({ ${deps.join(', ')} }) {
-						${
-							animation_frame && deindent`
-								cancelAnimationFrame(${animation_frame});
-								if (!${this.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`
-						}
-						${mutations.length > 0 && mutations}
-						${Array.from(dependencies).map(dep => `$$invalidate('${dep}', ${dep});`)}
+						${animation_frame && deindent`
+						cancelAnimationFrame(${animation_frame});
+						if (!${this.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`}
+						${needsLock && `${lock} = true;`}
+						ctx.${handler}.call(${this.var}${contextual_dependencies.size > 0 ? ', ctx' : ''});
 					}
 				`);
 
 				callee = handler;
 			} else {
-				this.renderer.component.partly_hoisted.push(deindent`
-					function ${handler}() {
-						${
-							animation_frame && deindent`
-								cancelAnimationFrame(${animation_frame});
-								if (!${this.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`
-						}
-						${mutations.length > 0 && mutations}
-						${Array.from(dependencies).map(dep => `$$invalidate('${dep}', ${dep});`)}
-					}
-				`);
-
 				callee = `ctx.${handler}`;
 			}
 
+			this.renderer.component.partly_hoisted.push(deindent`
+				function ${handler}(${contextual_dependencies.size > 0 ? `{ ${[...contextual_dependencies].join(', ')} }` : ``}) {
+					${group.bindings.map(b => b.handler.mutation)}
+					${Array.from(dependencies).map(dep => `$$invalidate('${dep}', ${dep});`)}
+				}
+			`);
+
 			group.events.forEach(name => {
 				if (name === 'resize') {
 					// special case
@@ -513,9 +467,10 @@ export default class ElementWrapper extends Wrapper {
 				.map(binding => `${binding.snippet} === void 0`)
 				.join(' || ');
 
-			if (this.node.name === 'select' || group.bindings.find(binding => binding.name === 'indeterminate' || binding.isReadOnlyMediaAttribute)) {
+			if (this.node.name === 'select' || group.bindings.find(binding => binding.node.name === 'indeterminate' || binding.isReadOnlyMediaAttribute())) {
+				const callback = has_local_function ? handler : `() => ${callee}.call(${this.var})`;
 				block.builders.hydrate.addLine(
-					`if (${someInitialStateIsUndefined}) @add_render_callback(() => ${callee}.call(${this.var}));`
+					`if (${someInitialStateIsUndefined}) @add_render_callback(${callback});`
 				);
 			}
 
@@ -526,7 +481,9 @@ export default class ElementWrapper extends Wrapper {
 			}
 		});
 
-		this.initialUpdate = mungedBindings.map(binding => binding.initialUpdate).filter(Boolean).join('\n');
+		if (lock) {
+			block.builders.update.addLine(`${lock} = false;`);
+		}
 
 		const this_binding = this.bindings.find(b => b.node.name === 'this');
 		if (this_binding) {
@@ -534,7 +491,7 @@ export default class ElementWrapper extends Wrapper {
 			renderer.component.declarations.push(name);
 			renderer.component.template_references.add(name);
 
-			const { handler, object } = this_binding.munge(block);
+			const { handler, object } = this_binding;
 
 			renderer.component.partly_hoisted.push(deindent`
 				function ${name}($$node) {
@@ -785,10 +742,6 @@ export default class ElementWrapper extends Wrapper {
 		return null;
 	}
 
-	isMediaNode() {
-		return this.node.name === 'audio' || this.node.name === 'video';
-	}
-
 	remount(name: string) {
 		const slot = this.attributes.find(attribute => attribute.node.name === 'slot');
 		if (slot) {
diff --git a/src/compile/render-dom/wrappers/InlineComponent/index.ts b/src/compile/render-dom/wrappers/InlineComponent/index.ts
--- a/src/compile/render-dom/wrappers/InlineComponent/index.ts
+++ b/src/compile/render-dom/wrappers/InlineComponent/index.ts
@@ -211,7 +211,7 @@ export default class InlineComponentWrapper extends Wrapper {
 					// bind:x={y} — we can't just do `y = x`, we need to
 					// to `array[index] = x;
 					const { name } = binding.expression.node;
-					const { object, property, snippet } = block.bindings.get(name)();
+					const { object, property, snippet } = block.bindings.get(name);
 					lhs = snippet;
 
 					// TODO we need to invalidate... something
@@ -263,7 +263,7 @@ export default class InlineComponentWrapper extends Wrapper {
 				// bind:x={y} — we can't just do `y = x`, we need to
 				// to `array[index] = x;
 				const { name } = binding.expression.node;
-				const { object, property, snippet } = block.bindings.get(name)();
+				const { object, property, snippet } = block.bindings.get(name);
 				lhs = snippet;
 				contextual_dependencies.push(object, property);
 			}


Test patch:
diff --git a/test/js/samples/input-files/expected.js b/test/js/samples/input-files/expected.js
--- a/test/js/samples/input-files/expected.js
+++ b/test/js/samples/input-files/expected.js
@@ -1,8 +1,8 @@
 /* generated by Svelte vX.Y.Z */
-import { SvelteComponent as SvelteComponent_1, addListener, createElement, detachNode, flush, init, insert, run, safe_not_equal, setAttribute } from "svelte/internal";
+import { SvelteComponent as SvelteComponent_1, addListener, createElement, detachNode, flush, init, insert, noop, run, safe_not_equal, setAttribute } from "svelte/internal";
 
 function create_fragment(component, ctx) {
-	var input, input_updating = false, current, dispose;
+	var input, current, dispose;
 
 	return {
 		c() {
@@ -14,15 +14,10 @@ function create_fragment(component, ctx) {
 
 		m(target, anchor) {
 			insert(target, input, anchor);
-
-			input.files = ctx.files;
-
 			current = true;
 		},
 
-		p(changed, ctx) {
-			if (!input_updating && changed.files) input.files = ctx.files;
-		},
+		p: noop,
 
 		i(target, anchor) {
 			if (current) return;
diff --git a/test/js/samples/media-bindings/expected.js b/test/js/samples/media-bindings/expected.js
--- a/test/js/samples/media-bindings/expected.js
+++ b/test/js/samples/media-bindings/expected.js
@@ -2,18 +2,25 @@
 import { SvelteComponent as SvelteComponent_1, addListener, add_render_callback, createElement, detachNode, flush, init, insert, run, run_all, safe_not_equal, timeRangesToArray } from "svelte/internal";
 
 function create_fragment(component, ctx) {
-	var audio, audio_is_paused = true, audio_updating = false, audio_animationframe, current, dispose;
+	var audio, audio_updating = false, audio_animationframe, audio_is_paused = true, current, dispose;
+
+	function audio_timeupdate_handler() {
+		cancelAnimationFrame(audio_animationframe);
+		if (!audio.paused) audio_animationframe = requestAnimationFrame(audio_timeupdate_handler);
+		audio_updating = true;
+		ctx.audio_timeupdate_handler.call(audio);
+	}
 
 	return {
 		c() {
 			audio = createElement("audio");
-			if (ctx.played === void 0 || ctx.currentTime === void 0) add_render_callback(() => ctx.audio_timeupdate_handler.call(audio));
+			if (ctx.played === void 0 || ctx.currentTime === void 0) add_render_callback(audio_timeupdate_handler);
 			if (ctx.duration === void 0) add_render_callback(() => ctx.audio_durationchange_handler.call(audio));
 			if (ctx.buffered === void 0) add_render_callback(() => ctx.audio_progress_handler.call(audio));
 			if (ctx.buffered === void 0 || ctx.seekable === void 0) add_render_callback(() => ctx.audio_loadedmetadata_handler.call(audio));
 
 			dispose = [
-				addListener(audio, "timeupdate", ctx.audio_timeupdate_handler),
+				addListener(audio, "timeupdate", audio_timeupdate_handler),
 				addListener(audio, "durationchange", ctx.audio_durationchange_handler),
 				addListener(audio, "play", ctx.audio_play_pause_handler),
 				addListener(audio, "pause", ctx.audio_play_pause_handler),
@@ -32,9 +39,10 @@ function create_fragment(component, ctx) {
 		},
 
 		p(changed, ctx) {
-			if (!audio_updating && !isNaN(ctx.currentTime) && changed.currentTime) audio.currentTime = ctx.currentTime;
-			if (!audio_updating && audio_is_paused !== (audio_is_paused = ctx.paused) && changed.paused) audio[audio_is_paused ? "pause" : "play"]();
-			if (!audio_updating && !isNaN(ctx.volume) && changed.volume) audio.volume = ctx.volume;
+			if (!audio_updating && changed.currentTime && !isNaN(ctx.currentTime)) audio.currentTime = ctx.currentTime;
+			if (changed.paused && audio_is_paused !== (audio_is_paused = ctx.paused)) audio[audio_is_paused ? "pause" : "play"]();
+			if (changed.volume && !isNaN(ctx.volume)) audio.volume = ctx.volume;
+			audio_updating = false;
 		},
 
 		i(target, anchor) {
@@ -58,8 +66,6 @@ function instance($$self, $$props, $$invalidate) {
 	let { buffered, seekable, played, currentTime, duration, paused, volume } = $$props;
 
 	function audio_timeupdate_handler() {
-		cancelAnimationFrame(audio_animationframe);
-		if (!audio.paused) audio_animationframe = requestAnimationFrame(audio_timeupdate_handler);
 		played = timeRangesToArray(this.played);
 		currentTime = this.currentTime;
 		$$invalidate('played', played);