start refactoring bindings

pull/922/head
Rich Harris 8 years ago
parent 744a2bb7f3
commit 40a5f28862

@ -183,7 +183,6 @@ export default function visitBinding(
if (!isMediaElement) { if (!isMediaElement) {
node.initialUpdate = updateElement; node.initialUpdate = updateElement;
node.initialUpdateNeedsStateObject = !block.contexts.has(name);
} }
if (!isReadOnly) { // audio/video duration is read-only, it never updates if (!isReadOnly) { // audio/video duration is read-only, it never updates

@ -4,9 +4,10 @@ import visitSlot from '../Slot';
import visitComponent from '../Component'; import visitComponent from '../Component';
import visitWindow from './meta/Window'; import visitWindow from './meta/Window';
import visitAttribute from './Attribute'; import visitAttribute from './Attribute';
import visitEventHandler from './EventHandler';
import visitBinding from './Binding'; import visitBinding from './Binding';
import visitRef from './Ref'; import addBindings from './addBindings';
import flattenReference from '../../../../utils/flattenReference';
import validCalleeObjects from '../../../../utils/validCalleeObjects';
import * as namespaces from '../../../../utils/namespaces'; import * as namespaces from '../../../../utils/namespaces';
import getStaticAttributeValue from '../../../../utils/getStaticAttributeValue'; import getStaticAttributeValue from '../../../../utils/getStaticAttributeValue';
import isVoidElementName from '../../../../utils/isVoidElementName'; import isVoidElementName from '../../../../utils/isVoidElementName';
@ -18,24 +19,10 @@ import { State } from '../../interfaces';
import reservedNames from '../../../../utils/reservedNames'; import reservedNames from '../../../../utils/reservedNames';
import { stringify } from '../../../../utils/stringify'; import { stringify } from '../../../../utils/stringify';
const meta = { const meta: Record<string, any> = {
':Window': visitWindow, ':Window': visitWindow,
}; };
const order = {
Attribute: 1,
Binding: 2,
EventHandler: 3,
Ref: 4,
};
const visitors = {
Attribute: visitAttribute,
EventHandler: visitEventHandler,
Binding: visitBinding,
Ref: visitRef,
};
export default function visitElement( export default function visitElement(
generator: DomGenerator, generator: DomGenerator,
block: Block, block: Block,
@ -69,8 +56,6 @@ export default function visitElement(
`${componentStack[componentStack.length - 1].var}._slotted.${slot.value[0].data}` : // TODO this looks bonkers `${componentStack[componentStack.length - 1].var}._slotted.${slot.value[0].data}` : // TODO this looks bonkers
state.parentNode; state.parentNode;
const isToplevel = !parentNode;
block.addVariable(name); block.addVariable(name);
block.builders.create.addLine( block.builders.create.addLine(
`${name} = ${getRenderStatement( `${name} = ${getRenderStatement(
@ -93,6 +78,10 @@ export default function visitElement(
); );
} else { } else {
block.builders.mount.addLine(`@insertNode(${name}, #target, anchor);`); block.builders.mount.addLine(`@insertNode(${name}, #target, anchor);`);
// TODO we eventually need to consider what happens to elements
// that belong to the same outgroup as an outroing element...
block.builders.unmount.addLine(`@detachNode(${name});`);
} }
// add CSS encapsulation attribute // add CSS encapsulation attribute
@ -109,24 +98,157 @@ export default function visitElement(
} }
} }
function visitAttributesAndAddProps() { if (node.name === 'textarea') {
let intro; // this is an egregious hack, but it's the easiest way to get <textarea>
let outro; // children treated the same way as a value attribute
if (node.children.length > 0) {
node.attributes node.attributes.push({
.sort((a: Node, b: Node) => order[a.type] - order[b.type]) type: 'Attribute',
.forEach((attribute: Node) => { name: 'value',
if (attribute.type === 'Transition') { value: node.children,
if (attribute.intro) intro = attribute; });
if (attribute.outro) outro = attribute;
return; node.children = [];
}
}
// insert static children with textContent or innerHTML
if (!childState.namespace && node.canUseInnerHTML && node.children.length > 0) {
if (node.children.length === 1 && node.children[0].type === 'Text') {
block.builders.create.addLine(
`${name}.textContent = ${stringify(node.children[0].data)};`
);
} else {
block.builders.create.addLine(
`${name}.innerHTML = ${stringify(node.children.map(toHTML).join(''))};`
);
}
} else {
node.children.forEach((child: Node) => {
visit(generator, block, childState, child, elementStack.concat(node), componentStack);
});
}
addBindings(generator, block, childState, node);
node.attributes.filter((a: Node) => a.type === 'Attribute').forEach((attribute: Node) => {
visitAttribute(generator, block, childState, node, attribute);
});
// event handlers
node.attributes.filter((a: Node) => a.type === 'EventHandler').forEach((attribute: Node) => {
const isCustomEvent = generator.events.has(attribute.name);
const shouldHoist = !isCustomEvent && state.inEachBlock;
const context = shouldHoist ? null : name;
const usedContexts: string[] = [];
if (attribute.expression) {
generator.addSourcemapLocations(attribute.expression);
const flattened = flattenReference(attribute.expression.callee);
if (!validCalleeObjects.has(flattened.name)) {
// 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) childState.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 (!~childState.allUsedContexts.indexOf(context))
childState.allUsedContexts.push(context);
});
});
}
const _this = context || 'this';
const declarations = usedContexts.map(name => {
if (name === 'state') {
if (shouldHoist) childState.usesComponent = true;
return `var state = ${block.alias('component')}.get();`;
}
const listName = block.listNames.get(name);
const indexName = block.indexNames.get(name);
const contextName = block.contexts.get(name);
return `var ${listName} = ${_this}._svelte.${listName}, ${indexName} = ${_this}._svelte.${indexName}, ${contextName} = ${listName}[${indexName}];`;
});
// get a name for the event handler that is globally unique
// if hoisted, locally unique otherwise
const handlerName = (shouldHoist ? generator : block).getUniqueName(
`${attribute.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
);
// create the handler body
const handlerBody = deindent`
${childState.usesComponent &&
`var ${block.alias('component')} = ${_this}._svelte.component;`}
${declarations}
${attribute.expression ?
`[✂${attribute.expression.start}-${attribute.expression.end}✂];` :
`${block.alias('component')}.fire("${attribute.name}", event);`}
`;
if (isCustomEvent) {
block.addVariable(handlerName);
block.builders.hydrate.addBlock(deindent`
${handlerName} = %events-${attribute.name}.call(#component, ${name}, function(event) {
${handlerBody}
});
`);
block.builders.destroy.addLine(deindent`
${handlerName}.teardown();
`);
} else {
const handler = deindent`
function ${handlerName}(event) {
${handlerBody}
}
`;
if (shouldHoist) {
generator.blocks.push(handler);
} else {
block.builders.init.addBlock(handler);
}
block.builders.hydrate.addLine(
`@addListener(${name}, "${attribute.name}", ${handlerName});`
);
block.builders.destroy.addLine(
`@removeListener(${name}, "${attribute.name}", ${handlerName});`
);
} }
});
visitors[attribute.type](generator, block, childState, node, attribute); // refs
node.attributes.filter((a: Node) => a.type === 'Ref').forEach((attribute: Node) => {
const ref = `#component.refs.${attribute.name}`;
block.builders.mount.addLine(
`${ref} = ${name};`
);
block.builders.destroy.addLine(
`if (${ref} === ${name}) ${ref} = null;`
);
generator.usesRefs = true; // so component.refs object is created
}); });
if (intro || outro) addTransitions(generator, block, childState, node);
addTransitions(generator, block, childState, node, intro, outro);
if (childState.allUsedContexts.length || childState.usesComponent) { if (childState.allUsedContexts.length || childState.usesComponent) {
const initialProps: string[] = []; const initialProps: string[] = [];
@ -162,53 +284,6 @@ export default function visitElement(
block.builders.update.addBlock(updates.join('\n')); block.builders.update.addBlock(updates.join('\n'));
} }
} }
}
if (isToplevel) {
// TODO we eventually need to consider what happens to elements
// that belong to the same outgroup as an outroing element...
block.builders.unmount.addLine(`@detachNode(${name});`);
}
if (node.name !== 'select') {
if (node.name === 'textarea') {
// this is an egregious hack, but it's the easiest way to get <textarea>
// children treated the same way as a value attribute
if (node.children.length > 0) {
node.attributes.push({
type: 'Attribute',
name: 'value',
value: node.children,
});
node.children = [];
}
}
// <select> value attributes are an annoying special case — it must be handled
// *after* its children have been updated
visitAttributesAndAddProps();
}
if (!childState.namespace && node.canUseInnerHTML && node.children.length > 0) {
if (node.children.length === 1 && node.children[0].type === 'Text') {
block.builders.create.addLine(
`${name}.textContent = ${stringify(node.children[0].data)};`
);
} else {
block.builders.create.addLine(
`${name}.innerHTML = ${stringify(node.children.map(toHTML).join(''))};`
);
}
} else {
node.children.forEach((child: Node) => {
visit(generator, block, childState, child, elementStack.concat(node), componentStack);
});
}
if (node.name === 'select') {
visitAttributesAndAddProps();
}
if (node.initialUpdate) { if (node.initialUpdate) {
block.builders.mount.addBlock(node.initialUpdate); block.builders.mount.addBlock(node.initialUpdate);

@ -1,111 +0,0 @@
import deindent from '../../../../utils/deindent';
import flattenReference from '../../../../utils/flattenReference';
import validCalleeObjects from '../../../../utils/validCalleeObjects';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
export default function visitEventHandler(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute: Node
) {
const name = attribute.name;
const isCustomEvent = generator.events.has(name);
const shouldHoist = !isCustomEvent && state.inEachBlock;
const context = shouldHoist ? null : state.parentNode;
const usedContexts: string[] = [];
if (attribute.expression) {
generator.addSourcemapLocations(attribute.expression);
const flattened = flattenReference(attribute.expression.callee);
if (!validCalleeObjects.has(flattened.name)) {
// 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 => {
if (name === 'state') {
if (shouldHoist) state.usesComponent = true;
return `var state = ${block.alias('component')}.get();`;
}
const listName = block.listNames.get(name);
const indexName = block.indexNames.get(name);
const contextName = block.contexts.get(name);
return `var ${listName} = ${_this}._svelte.${listName}, ${indexName} = ${_this}._svelte.${indexName}, ${contextName} = ${listName}[${indexName}];`;
});
// get a name for the event handler that is globally unique
// if hoisted, locally unique otherwise
const handlerName = (shouldHoist ? generator : block).getUniqueName(
`${name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler`
);
// create the handler body
const handlerBody = deindent`
${state.usesComponent &&
`var ${block.alias('component')} = ${_this}._svelte.component;`}
${declarations}
${attribute.expression ?
`[✂${attribute.expression.start}-${attribute.expression.end}✂];` :
`${block.alias('component')}.fire("${attribute.name}", event);`}
`;
if (isCustomEvent) {
block.addVariable(handlerName);
block.builders.hydrate.addBlock(deindent`
${handlerName} = %events-${name}.call(#component, ${state.parentNode}, function(event) {
${handlerBody}
});
`);
block.builders.destroy.addLine(deindent`
${handlerName}.teardown();
`);
} else {
const handler = deindent`
function ${handlerName}(event) {
${handlerBody}
}
`;
if (shouldHoist) {
generator.blocks.push(handler);
} else {
block.builders.init.addBlock(handler);
}
block.builders.hydrate.addLine(
`@addListener(${state.parentNode}, "${name}", ${handlerName});`
);
block.builders.destroy.addLine(
`@removeListener(${state.parentNode}, "${name}", ${handlerName});`
);
}
}

@ -1,25 +0,0 @@
import deindent from '../../../../utils/deindent';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
export default function visitRef(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute: Node
) {
const name = attribute.name;
block.builders.mount.addLine(
`#component.refs.${name} = ${state.parentNode};`
);
block.builders.destroy.addLine(deindent`
if (#component.refs.${name} === ${state.parentNode}) #component.refs.${name} = null;
`);
generator.usesRefs = true; // so this component.refs object is created
}

@ -0,0 +1,689 @@
import deindent from '../../../../utils/deindent';
import flattenReference from '../../../../utils/flattenReference';
import getStaticAttributeValue from '../../../../utils/getStaticAttributeValue';
import { DomGenerator } from '../../index';
import Block from '../../Block';
import { Node } from '../../../../interfaces';
import { State } from '../../interfaces';
import getObject from '../../../../utils/getObject';
import getTailSnippet from '../../../../utils/getTailSnippet';
import visitBinding from './Binding';
import { generateRule } from '../../../../shared/index';
const types: Record<string, (
generator: DomGenerator,
block: Block,
state: State,
node: Node,
binding: Node[]
) => void> = {
input: addInputBinding,
select: addSelectBinding,
audio: addMediaBinding,
video: addMediaBinding
};
const readOnlyMediaAttributes = new Set([
'duration',
'buffered',
'seekable',
'played'
]);
export default function addBindings(
generator: DomGenerator,
block: Block,
state: State,
node: Node
) {
const bindings: Node[] = node.attributes.filter((a: Node) => a.type === 'Binding');
if (bindings.length === 0) return;
types[node.name](generator, block, state, node, bindings);
}
function addInputBinding(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
bindings: Node[]
) {
const attribute = bindings[0];
const { name } = getObject(attribute.value);
const { snippet, contexts, dependencies } = block.contextualise(
attribute.value
);
contexts.forEach(context => {
if (!~state.allUsedContexts.indexOf(context))
state.allUsedContexts.push(context);
});
const type = getStaticAttributeValue(node, 'type');
const eventName = type === 'radio' || type === 'checkbox' ? 'change' : 'input';
const handler = block.getUniqueName(
`${node.var}_${eventName}_handler`
);
const bindingGroup = attribute.name === 'group'
? getBindingGroup(generator, attribute.value)
: null;
const value = (
attribute.name === 'group' ?
(type === 'checkbox' ? `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])` : `${node.var}.__value`) :
(type === 'range' || type === 'number') ?
`@toNumber(${node.var}.${attribute.name})` :
`${node.var}.${attribute.name}`
);
let setter = getSetter(generator, block, name, snippet, node, attribute, dependencies, value);
let updateElement = `${node.var}.${attribute.name} = ${snippet};`;
const needsLock = !/radio|checkbox|range|color/.test(type); // TODO others?
const lock = `#${node.var}_updating`;
let updateConditions = needsLock ? [`!${lock}`] : [];
if (needsLock) block.addVariable(lock, 'false');
if (attribute.name === 'group') {
// <input type='checkbox|radio' bind:group='selected'> special case
if (type === 'radio') {
setter = deindent`
if (!${node.var}.checked) return;
${setter}
`;
}
const condition = type === 'checkbox'
? `~${snippet}.indexOf(${node.var}.__value)`
: `${node.var}.__value === ${snippet}`;
block.builders.hydrate.addLine(
`#component._bindingGroups[${bindingGroup}].push(${node.var});`
);
block.builders.destroy.addBlock(
`#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${node.var}), 1);`
);
updateElement = `${node.var}.checked = ${condition};`;
}
block.builders.init.addBlock(deindent`
function ${handler}() {
${needsLock && `${lock} = true;`}
${setter}
${needsLock && `${lock} = false;`}
}
`);
if (type === 'range') {
// need to bind to `input` and `change`, for the benefit of IE
block.builders.hydrate.addBlock(deindent`
@addListener(${node.var}, "input", ${handler});
@addListener(${node.var}, "change", ${handler});
`);
block.builders.destroy.addBlock(deindent`
@removeListener(${node.var}, "input", ${handler});
@removeListener(${node.var}, "change", ${handler});
`);
} else {
block.builders.hydrate.addLine(
`@addListener(${node.var}, "${eventName}", ${handler});`
);
block.builders.destroy.addLine(
`@removeListener(${node.var}, "${eventName}", ${handler});`
);
}
node.initialUpdate = updateElement;
if (updateConditions.length) {
block.builders.update.addBlock(deindent`
if (${updateConditions.join(' && ')}) {
${updateElement}
}
`);
} else {
block.builders.update.addBlock(deindent`
${updateElement}
`);
}
}
function addSelectBinding(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
bindings: Node[]
) {
const attribute = bindings[0];
const { name } = getObject(attribute.value);
const { snippet, contexts, dependencies } = block.contextualise(
attribute.value
);
contexts.forEach(context => {
if (!~state.allUsedContexts.indexOf(context))
state.allUsedContexts.push(context);
});
const eventNames = getBindingEventName(node, attribute);
const handler = block.getUniqueName(
`${node.var}_${eventNames.join('_')}_handler`
);
const isMultipleSelect =
node.name === 'select' &&
node.attributes.find(
(attr: Node) => attr.name.toLowerCase() === 'multiple'
); // TODO use getStaticAttributeValue
const type = getStaticAttributeValue(node, 'type');
const bindingGroup = attribute.name === 'group'
? getBindingGroup(generator, attribute.value)
: null;
const isMediaElement = node.name === 'audio' || node.name === 'video';
const isReadOnly = isMediaElement && readOnlyMediaAttributes.has(attribute.name)
const value = getBindingValue(
generator,
block,
state,
node,
attribute,
isMultipleSelect,
isMediaElement,
bindingGroup,
type
);
let setter = getSetter(generator, block, name, snippet, node, attribute, dependencies, value);
let updateElement = `${node.var}.${attribute.name} = ${snippet};`;
const needsLock = !isReadOnly && node.name !== 'input' || !/radio|checkbox|range|color/.test(type); // TODO others?
const lock = `#${node.var}_updating`;
let updateConditions = needsLock ? [`!${lock}`] : [];
if (needsLock) block.addVariable(lock, 'false');
// <select> special case
if (node.name === 'select') {
if (!isMultipleSelect) {
setter = `var selectedOption = ${node.var}.querySelector(':checked') || ${node.var}.options[0];\n${setter}`;
}
const value = block.getUniqueName('value');
const option = block.getUniqueName('option');
const ifStatement = isMultipleSelect
? deindent`
${option}.selected = ~${value}.indexOf(${option}.__value);`
: deindent`
if (${option}.__value === ${value}) {
${option}.selected = true;
break;
}`;
const { name } = getObject(attribute.value);
const tailSnippet = getTailSnippet(attribute.value);
updateElement = deindent`
var ${value} = ${snippet};
for (var #i = 0; #i < ${node.var}.options.length; #i += 1) {
var ${option} = ${node.var}.options[#i];
${ifStatement}
}
`;
generator.hasComplexBindings = true;
block.builders.hydrate.addBlock(
`if (!('${name}' in state)) #component._root._beforecreate.push(${handler});`
);
} else if (attribute.name === 'group') {
// <input type='checkbox|radio' bind:group='selected'> special case
if (type === 'radio') {
setter = deindent`
if (!${node.var}.checked) return;
${setter}
`;
}
const condition = type === 'checkbox'
? `~${snippet}.indexOf(${node.var}.__value)`
: `${node.var}.__value === ${snippet}`;
block.builders.hydrate.addLine(
`#component._bindingGroups[${bindingGroup}].push(${node.var});`
);
block.builders.destroy.addBlock(
`#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${node.var}), 1);`
);
updateElement = `${node.var}.checked = ${condition};`;
} else if (isMediaElement) {
generator.hasComplexBindings = true;
block.builders.hydrate.addBlock(`#component._root._beforecreate.push(${handler});`);
if (attribute.name === 'currentTime') {
const frame = block.getUniqueName(`${node.var}_animationframe`);
block.addVariable(frame);
setter = deindent`
cancelAnimationFrame(${frame});
if (!${node.var}.paused) ${frame} = requestAnimationFrame(${handler});
${setter}
`;
updateConditions.push(`!isNaN(${snippet})`);
} else if (attribute.name === 'paused') {
// this is necessary to prevent the audio restarting by itself
const last = block.getUniqueName(`${node.var}_paused_value`);
block.addVariable(last, 'true');
updateConditions = [`${last} !== (${last} = ${snippet})`];
updateElement = `${node.var}[${last} ? "pause" : "play"]();`;
}
}
block.builders.init.addBlock(deindent`
function ${handler}() {
${needsLock && `${lock} = true;`}
${setter}
${needsLock && `${lock} = false;`}
}
`);
if (node.name === 'input' && type === 'range') {
// need to bind to `input` and `change`, for the benefit of IE
block.builders.hydrate.addBlock(deindent`
@addListener(${node.var}, "input", ${handler});
@addListener(${node.var}, "change", ${handler});
`);
block.builders.destroy.addBlock(deindent`
@removeListener(${node.var}, "input", ${handler});
@removeListener(${node.var}, "change", ${handler});
`);
} else {
eventNames.forEach(eventName => {
block.builders.hydrate.addLine(
`@addListener(${node.var}, "${eventName}", ${handler});`
);
block.builders.destroy.addLine(
`@removeListener(${node.var}, "${eventName}", ${handler});`
);
});
}
if (!isMediaElement) {
node.initialUpdate = updateElement;
}
if (!isReadOnly) { // audio/video duration is read-only, it never updates
if (updateConditions.length) {
block.builders.update.addBlock(deindent`
if (${updateConditions.join(' && ')}) {
${updateElement}
}
`);
} else {
block.builders.update.addBlock(deindent`
${updateElement}
`);
}
}
if (attribute.name === 'paused') {
block.builders.create.addLine(
`@addListener(${node.var}, "play", ${handler});`
);
block.builders.destroy.addLine(
`@removeListener(${node.var}, "play", ${handler});`
);
}
}
function addMediaBinding(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
bindings: Node[]
) {
const attribute = bindings[0];
const { name } = getObject(attribute.value);
const { snippet, contexts, dependencies } = block.contextualise(
attribute.value
);
contexts.forEach(context => {
if (!~state.allUsedContexts.indexOf(context))
state.allUsedContexts.push(context);
});
const eventNames = getBindingEventName(node, attribute);
const handler = block.getUniqueName(
`${node.var}_${eventNames.join('_')}_handler`
);
const isMultipleSelect =
node.name === 'select' &&
node.attributes.find(
(attr: Node) => attr.name.toLowerCase() === 'multiple'
); // TODO use getStaticAttributeValue
const type = getStaticAttributeValue(node, 'type');
const bindingGroup = attribute.name === 'group'
? getBindingGroup(generator, attribute.value)
: null;
const isMediaElement = node.name === 'audio' || node.name === 'video';
const isReadOnly = isMediaElement && readOnlyMediaAttributes.has(attribute.name)
const value = getBindingValue(
generator,
block,
state,
node,
attribute,
isMultipleSelect,
isMediaElement,
bindingGroup,
type
);
let setter = getSetter(generator, block, name, snippet, node, attribute, dependencies, value);
let updateElement = `${node.var}.${attribute.name} = ${snippet};`;
const needsLock = !isReadOnly && node.name !== 'input' || !/radio|checkbox|range|color/.test(type); // TODO others?
const lock = `#${node.var}_updating`;
let updateConditions = needsLock ? [`!${lock}`] : [];
if (needsLock) block.addVariable(lock, 'false');
// <select> special case
if (node.name === 'select') {
if (!isMultipleSelect) {
setter = `var selectedOption = ${node.var}.querySelector(':checked') || ${node.var}.options[0];\n${setter}`;
}
const value = block.getUniqueName('value');
const option = block.getUniqueName('option');
const ifStatement = isMultipleSelect
? deindent`
${option}.selected = ~${value}.indexOf(${option}.__value);`
: deindent`
if (${option}.__value === ${value}) {
${option}.selected = true;
break;
}`;
const { name } = getObject(attribute.value);
const tailSnippet = getTailSnippet(attribute.value);
updateElement = deindent`
var ${value} = ${snippet};
for (var #i = 0; #i < ${node.var}.options.length; #i += 1) {
var ${option} = ${node.var}.options[#i];
${ifStatement}
}
`;
generator.hasComplexBindings = true;
block.builders.hydrate.addBlock(
`if (!('${name}' in state)) #component._root._beforecreate.push(${handler});`
);
} else if (attribute.name === 'group') {
// <input type='checkbox|radio' bind:group='selected'> special case
if (type === 'radio') {
setter = deindent`
if (!${node.var}.checked) return;
${setter}
`;
}
const condition = type === 'checkbox'
? `~${snippet}.indexOf(${node.var}.__value)`
: `${node.var}.__value === ${snippet}`;
block.builders.hydrate.addLine(
`#component._bindingGroups[${bindingGroup}].push(${node.var});`
);
block.builders.destroy.addBlock(
`#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${node.var}), 1);`
);
updateElement = `${node.var}.checked = ${condition};`;
} else if (isMediaElement) {
generator.hasComplexBindings = true;
block.builders.hydrate.addBlock(`#component._root._beforecreate.push(${handler});`);
if (attribute.name === 'currentTime') {
const frame = block.getUniqueName(`${node.var}_animationframe`);
block.addVariable(frame);
setter = deindent`
cancelAnimationFrame(${frame});
if (!${node.var}.paused) ${frame} = requestAnimationFrame(${handler});
${setter}
`;
updateConditions.push(`!isNaN(${snippet})`);
} else if (attribute.name === 'paused') {
// this is necessary to prevent the audio restarting by itself
const last = block.getUniqueName(`${node.var}_paused_value`);
block.addVariable(last, 'true');
updateConditions = [`${last} !== (${last} = ${snippet})`];
updateElement = `${node.var}[${last} ? "pause" : "play"]();`;
}
}
block.builders.init.addBlock(deindent`
function ${handler}() {
${needsLock && `${lock} = true;`}
${setter}
${needsLock && `${lock} = false;`}
}
`);
if (node.name === 'input' && type === 'range') {
// need to bind to `input` and `change`, for the benefit of IE
block.builders.hydrate.addBlock(deindent`
@addListener(${node.var}, "input", ${handler});
@addListener(${node.var}, "change", ${handler});
`);
block.builders.destroy.addBlock(deindent`
@removeListener(${node.var}, "input", ${handler});
@removeListener(${node.var}, "change", ${handler});
`);
} else {
eventNames.forEach(eventName => {
block.builders.hydrate.addLine(
`@addListener(${node.var}, "${eventName}", ${handler});`
);
block.builders.destroy.addLine(
`@removeListener(${node.var}, "${eventName}", ${handler});`
);
});
}
if (!isMediaElement) {
node.initialUpdate = updateElement;
}
if (!isReadOnly) { // audio/video duration is read-only, it never updates
if (updateConditions.length) {
block.builders.update.addBlock(deindent`
if (${updateConditions.join(' && ')}) {
${updateElement}
}
`);
} else {
block.builders.update.addBlock(deindent`
${updateElement}
`);
}
}
if (attribute.name === 'paused') {
block.builders.create.addLine(
`@addListener(${node.var}, "play", ${handler});`
);
block.builders.destroy.addLine(
`@removeListener(${node.var}, "play", ${handler});`
);
}
}
function getBindingEventName(node: Node, attribute: Node) {
if (node.name === 'input') {
const typeAttribute = node.attributes.find(
(attr: Node) => attr.type === 'Attribute' && attr.name === 'type'
);
const type = typeAttribute ? typeAttribute.value[0].data : 'text'; // TODO in validation, should throw if type attribute is not static
return [type === 'checkbox' || type === 'radio' ? 'change' : 'input'];
}
if (node.name === 'textarea') return ['input'];
if (attribute.name === 'currentTime') return ['timeupdate'];
if (attribute.name === 'duration') return ['durationchange'];
if (attribute.name === 'paused') return ['pause'];
if (attribute.name === 'buffered') return ['progress', 'loadedmetadata'];
if (attribute.name === 'seekable') return ['loadedmetadata'];
if (attribute.name === 'played') return ['timeupdate'];
return ['change'];
}
function getBindingValue(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
attribute: Node,
isMultipleSelect: boolean,
isMediaElement: boolean,
bindingGroup: number,
type: string
) {
// <select multiple bind:value='selected>
if (isMultipleSelect) {
return `[].map.call(${node.var}.querySelectorAll(':checked'), function(option) { return option.__value; })`;
}
// <select bind:value='selected>
if (node.name === 'select') {
return 'selectedOption && selectedOption.__value';
}
// <input type='checkbox' bind:group='foo'>
if (attribute.name === 'group') {
if (type === 'checkbox') {
return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`;
}
return `${node.var}.__value`;
}
// <input type='range|number' bind:value>
if (type === 'range' || type === 'number') {
return `@toNumber(${node.var}.${attribute.name})`;
}
if (isMediaElement && (attribute.name === 'buffered' || attribute.name === 'seekable' || attribute.name === 'played')) {
return `@timeRangesToArray(${node.var}.${attribute.name})`
}
// everything else
return `${node.var}.${attribute.name}`;
}
function getBindingGroup(generator: DomGenerator, value: Node) {
const { parts } = flattenReference(value); // TODO handle cases involving computed member expressions
const keypath = parts.join('.');
// TODO handle contextual bindings — `keypath` should include unique ID of
// each block that provides context
let index = generator.bindingGroups.indexOf(keypath);
if (index === -1) {
index = generator.bindingGroups.length;
generator.bindingGroups.push(keypath);
}
return index;
}
function getSetter(
generator: DomGenerator,
block: Block,
name: string,
snippet: string,
node: Node,
attribute: Node,
dependencies: string[],
value: string,
) {
const tail = attribute.value.type === 'MemberExpression'
? getTailSnippet(attribute.value)
: '';
if (block.contexts.has(name)) {
const prop = dependencies[0];
const computed = isComputed(attribute.value);
return deindent`
var list = ${node.var}._svelte.${block.listNames.get(name)};
var index = ${node.var}._svelte.${block.indexNames.get(name)};
${computed && `var state = #component.get();`}
list[index]${tail} = ${value};
${computed
? `#component.set({${dependencies.map((prop: string) => `${prop}: state.${prop}`).join(', ')} });`
: `#component.set({${dependencies.map((prop: string) => `${prop}: #component.get('${prop}')`).join(', ')} });`}
`;
}
if (attribute.value.type === 'MemberExpression') {
// This is a little confusing, and should probably be tidied up
// at some point. It addresses a tricky bug (#893), wherein
// Svelte tries to `set()` a computed property, which throws an
// error in dev mode. a) it's possible that we should be
// replacing computations with *their* dependencies, and b)
// we should probably populate `generator.readonly` sooner so
// that we don't have to do the `.some()` here
dependencies = dependencies.filter(prop => !generator.computations.some(computation => computation.key === prop));
return deindent`
var state = #component.get();
${snippet} = ${value};
#component.set({ ${dependencies.map((prop: string) => `${prop}: state.${prop}`).join(', ')} });
`;
}
return `#component.set({ ${name}: ${value} });`;
}
function isComputed(node: Node) {
while (node.type === 'MemberExpression') {
if (node.computed) return true;
node = node.object;
}
return false;
}

@ -8,10 +8,13 @@ export default function addTransitions(
generator: DomGenerator, generator: DomGenerator,
block: Block, block: Block,
state: State, state: State,
node: Node, node: Node
intro,
outro
) { ) {
const intro = node.attributes.find((a: Node) => a.type === 'Transition' && a.intro);
const outro = node.attributes.find((a: Node) => a.type === 'Transition' && a.outro);
if (!intro && !outro) return;
if (intro === outro) { if (intro === outro) {
const name = block.getUniqueName(`${node.var}_transition`); const name = block.getUniqueName(`${node.var}_transition`);
const snippet = intro.expression const snippet = intro.expression

Loading…
Cancel
Save