abandon attempt to refactor bindings more aggressively

pull/1746/head
Rich Harris 8 years ago
parent f4a87d0d9d
commit 7e75e2dc27

@ -1,365 +0,0 @@
import ElementWrapper from '..';
import Block from '../../../Block';
import deindent from '../../../../../utils/deindent';
import Binding from '../../../../nodes/Binding';
import getObject from '../../../../../utils/getObject';
import Renderer from '../../../Renderer';
import getTailSnippet from '../../../../../utils/getTailSnippet';
import Element from '../../../../nodes/Element';
import { dimensions } from '../../../../../utils/patterns';
// TODO this should live in a specific binding
const readOnlyMediaAttributes = new Set([
'duration',
'buffered',
'seekable',
'played'
]);
export default class BindingWrapper {
element: ElementWrapper;
binding: Binding;
events: string[];
dependencies: Set<string>;
usesStore: boolean;
needsLock: boolean;
lock: string;
mutations: string[];
props: Set<string>;
storeProps: Set<string>;
handlerName: string;
object: string;
// handler: Handler;
updateDom: string;
initialUpdate: string;
updateCondition: string;
isReadOnly: boolean;
constructor(block: Block, element: ElementWrapper, binding: Binding) {
this.element = element;
this.binding = binding;
element.cannotUseInnerHTML();
this.isReadOnly = false;
this.needsLock = false;
this.events = [];
this.dependencies = new Set(this.binding.value.dependencies);
// 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?
this.binding.value.dependencies.forEach((prop: string) => {
const indirectDependencies = this.element.renderer.component.indirectDependencies.get(prop);
if (indirectDependencies) {
indirectDependencies.forEach(indirectDependency => {
this.dependencies.add(indirectDependency);
});
}
});
block.addDependencies(this.dependencies);
}
fromDom() {
throw new Error(`TODO implement in subclass`);
// <select bind:value='selected>
if (this.element.node.name === 'select') {
return this.element.node.getStaticAttributeValue('multiple') === true ?
`@selectMultipleValue(${this.element.var})` :
`@selectValue(${this.element.var})`;
}
const type = this.element.node.getStaticAttributeValue('type');
// <input type='range|number' bind:value>
if (type === 'range' || type === 'number') {
return `@toNumber(${node.var}.${binding.name})`;
}
if ((binding.name === 'buffered' || binding.name === 'seekable' || binding.name === 'played')) {
return `@timeRangesToArray(${node.var}.${binding.name})`
}
// everything else
return `${node.var}.${binding.name}`;
}
toDom() {
throw new Error(`TODO implement in subclass`);
if (binding.isReadOnlyMediaAttribute()) {
return null;
}
if (node.name === 'select') {
return node.getStaticAttributeValue('multiple') === true ?
`@selectOptions(${node.var}, ${snippet})` :
`@selectOption(${node.var}, ${snippet})`;
}
if (binding.name === 'group') {
const type = node.getStaticAttributeValue('type');
const condition = type === 'checkbox'
? `~${snippet}.indexOf(${node.var}.__value)`
: `${node.var}.__value === ${snippet}`;
return `${node.var}.checked = ${condition};`
}
return `${node.var}.${binding.name} = ${snippet};`;
}
isReadOnlyMediaAttribute() {
return readOnlyMediaAttributes.has(this.binding.name);
}
render(block: Block) {
// const needsLock = (
// parent.node.name !== 'input' ||
// !/radio|checkbox|range|color/.test(parent.getStaticAttributeValue('type'))
// );
// const isReadOnly = (
// (parent.isMediaNode() && readOnlyMediaAttributes.has(this.node.name)) ||
// dimensions.test(this.node.name)
// );
const lock = this.needsLock ?
block.getUniqueName(`${this.element.var}_updating`) :
null;
if (lock) block.addVariable(lock, 'false');
const updateConditions: string[] = [];
if (lock) updateConditions.push(`!${lock}`);
const { name } = getObject(this.binding.value.node);
const { snippet } = this.binding.value;
// view to model
const valueFromDom = this.fromDom();
const handler = getEventHandler(this.binding, this.element.renderer, block, name, snippet, this.dependencies, valueFromDom);
// model to view
let updateDom = this.toDom();
let initialUpdate = updateDom;
if (this.binding.name === 'currentTime' || this.binding.name === 'volume') {
updateConditions.push(`!isNaN(${snippet})`);
if (this.binding.name === 'currentTime') initialUpdate = null;
}
if (this.binding.name === 'paused') {
// this is necessary to prevent audio restarting by itself
const last = block.getUniqueName(`${this.element.var}_is_paused`);
block.addVariable(last, 'true');
updateConditions.push(`${last} !== (${last} = ${snippet})`);
updateDom = `${this.element.var}[${last} ? "pause" : "play"]();`;
initialUpdate = null;
}
// bind:offsetWidth and bind:offsetHeight
if (dimensions.test(this.binding.name)) {
initialUpdate = null;
updateDom = null;
}
const dependencyArray = [...this.binding.value.dependencies]
if (dependencyArray.length === 1) {
updateConditions.push(`changed.${dependencyArray[0]}`)
} else if (dependencyArray.length > 1) {
updateConditions.push(
`(${dependencyArray.map(prop => `changed.${prop}`).join(' || ')})`
)
}
this.object = name;
this.handler = handler;
this.updateDom = updateDom;
this.initialUpdate = initialUpdate;
this.needsLock = !this.isReadOnly && this.needsLock; // TODO ????
this.updateCondition = updateConditions.length ? updateConditions.join(' && ') : undefined;
let animation_frame = null; // TODO media binding only
this.handlerName = block.getUniqueName(`${this.element.var}_${this.events.join('_')}_handler`);
block.builders.init.addBlock(deindent`
function ${this.handlerName}() {
${
animation_frame && deindent`
cancelAnimationFrame(${animation_frame});
if (!${this.element.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`
}
${handler.usesStore && `var $ = #component.store.get();`}
${this.needsLock && `${lock} = true;`}
${handler.mutation}
${handler.props.length > 0 && `#component.set({ ${handler.props.join(', ')} });`}
${handler.storeProps.length > 0 && `#component.store.set({ ${handler.storeProps.join(', ')} });`}
${this.needsLock && `${lock} = false;`}
}
`);
block.builders.update.addLine(
updateConditions.length ? `if (${updateConditions.join(' && ')}) ${this.updateDom}` : this.updateDom
);
this.events.forEach(name => {
block.builders.hydrate.addLine(
`@addListener(${this.element.var}, "${name}", ${this.handlerName});`
);
block.builders.destroy.addLine(
`@removeListener(${this.element.var}, "${name}", ${this.handlerName});`
);
});
}
}
function getEventHandler(
binding: Binding,
renderer: Renderer,
block: Block,
name: string,
snippet: string,
dependencies: Set<string>,
value: string
) {
const storeDependencies = [...dependencies].filter(prop => prop[0] === '$').map(prop => prop.slice(1));
let dependenciesArray = [...dependencies].filter(prop => prop[0] !== '$');
if (binding.isContextual) {
const tail = binding.value.node.type === 'MemberExpression'
? getTailSnippet(binding.value.node)
: '';
const head = block.bindings.get(name);
return {
usesContext: true,
usesState: true,
usesStore: storeDependencies.length > 0,
mutation: `${head()}${tail} = ${value};`,
props: dependenciesArray.map(prop => `${prop}: ctx.${prop}`),
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
};
}
if (binding.value.node.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 `component.target.readonly` sooner so
// that we don't have to do the `.some()` here
dependenciesArray = dependenciesArray.filter(prop => !renderer.component.computations.some(computation => computation.key === prop));
return {
usesContext: false,
usesState: true,
usesStore: storeDependencies.length > 0,
mutation: `${snippet} = ${value}`,
props: dependenciesArray.map((prop: string) => `${prop}: ctx.${prop}`),
storeProps: storeDependencies.map(prop => `${prop}: $.${prop}`)
};
}
let props;
let storeProps;
if (name[0] === '$') {
props = [];
storeProps = [`${name.slice(1)}: ${value}`];
} else {
props = [`${name}: ${value}`];
storeProps = [];
}
return {
usesContext: false,
usesState: false,
usesStore: false,
mutation: null,
props,
storeProps
};
}
// function fromDom(
// renderer: Renderer,
// node: Element,
// binding: Binding
// ) {
// // <select bind:value='selected>
// if (node.name === 'select') {
// return node.getStaticAttributeValue('multiple') === true ?
// `@selectMultipleValue(${node.var})` :
// `@selectValue(${node.var})`;
// }
// const type = node.getStaticAttributeValue('type');
// // <input type='checkbox' bind:group='foo'>
// if (binding.name === 'group') {
// const bindingGroup = getBindingGroup(renderer, binding.value.node);
// 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}.${binding.name})`;
// }
// if ((binding.name === 'buffered' || binding.name === 'seekable' || binding.name === 'played')) {
// return `@timeRangesToArray(${node.var}.${binding.name})`
// }
// // everything else
// return `${node.var}.${binding.name}`;
// }
function getDomUpdater(
node: Element,
binding: Binding,
snippet: string
) {
if (binding.isReadOnlyMediaAttribute()) {
return null;
}
if (node.name === 'select') {
return node.getStaticAttributeValue('multiple') === true ?
`@selectOptions(${node.var}, ${snippet})` :
`@selectOption(${node.var}, ${snippet})`;
}
if (binding.name === 'group') {
const type = node.getStaticAttributeValue('type');
const condition = type === 'checkbox'
? `~${snippet}.indexOf(${node.var}.__value)`
: `${node.var}.__value === ${snippet}`;
return `${node.var}.checked = ${condition};`
}
return `${node.var}.${binding.name} = ${snippet};`;
}

@ -1,37 +0,0 @@
import Binding from '../../../../nodes/Binding';
import Element from '../../../../nodes/Element';
import ElementWrapper from '..';
import BindingWrapper from './Binding';
import Block from '../../../Block';
export default class InputCheckboxBinding extends BindingWrapper {
events = ['change'];
static filter(
node: Element,
binding_lookup: Record<string, Binding>,
type: string
) {
return (
node.name === 'input' &&
binding_lookup.checked &&
type === 'checkbox'
);
}
constructor(
block: Block,
element: ElementWrapper,
binding_lookup: Record<string, Binding>
) {
super(block, element, binding_lookup.checked);
}
fromDom() {
return `${this.element.var}.checked`;
}
toDom() {
return `${this.element.var}.checked = ${this.binding.value.snippet};`;
}
}

@ -1,29 +0,0 @@
import Binding from '../../../../nodes/Binding';
import Element from '../../../../nodes/Element';
import ElementWrapper from '..';
import InputRadioGroupBinding from './InputRadioGroupBinding';
export default class InputCheckboxGroupBinding extends InputRadioGroupBinding {
element: ElementWrapper;
node: Binding;
static filter(
node: Element,
binding_lookup: Record<string, Binding>,
type: string
) {
if (node.name === 'input' && binding_lookup.group) {
return type === 'checkbox';
}
}
fromDom() {
return `@getBindingGroupValue(#component._bindingGroups[${this.bindingGroup}])`;
}
toDom() {
const condition = `~${this.binding.value.snippet}.indexOf(${this.element.var}.__value)`;
return `${this.element.var}.checked = ${condition};`;
}
}

@ -1,38 +0,0 @@
import Binding from '../../../../nodes/Binding';
import Element from '../../../../nodes/Element';
import ElementWrapper from '..';
import BindingWrapper from './Binding';
import Block from '../../../Block';
export default class InputNumberBinding extends BindingWrapper {
events = ['input'];
static filter(
node: Element,
binding_lookup: Record<string, Binding>,
type: string
) {
return (
node.name === 'input' &&
type === 'number' &&
binding_lookup.value
);
}
constructor(
block: Block,
element: ElementWrapper,
binding_lookup: Record<string, Binding>
) {
super(block, element, binding_lookup.value);
this.needsLock = true;
}
fromDom() {
return `@toNumber(${this.element.var}.value)`;
}
toDom() {
return `${this.element.var}.value = ${this.binding.value.snippet};`;
}
}

@ -1,70 +0,0 @@
import Binding from '../../../../nodes/Binding';
import Element from '../../../../nodes/Element';
import ElementWrapper from '..';
import Block from '../../../Block';
import Renderer from '../../../Renderer';
import flattenReference from '../../../../../utils/flattenReference';
import { Node } from '../../../../../interfaces';
import BindingWrapper from './Binding';
export default class InputRadioGroupBinding extends BindingWrapper {
element: ElementWrapper;
node: Binding;
bindingGroup: number;
events = ['change'];
static filter(
node: Element,
binding_lookup: Record<string, Binding>,
type: string
) {
if (node.name === 'input' && binding_lookup.group) {
return type === 'radio';
}
}
constructor(
block: Block,
element: ElementWrapper,
binding_lookup: Record<string, Binding>
) {
super(block, element, binding_lookup.group);
// TODO handle cases involving computed member expressions
const { parts } = flattenReference(this.binding.value.node);
const keypath = parts.join('.');
// TODO handle contextual bindings — `keypath` should include unique ID of
// each block that provides context
let index = element.renderer.bindingGroups.indexOf(keypath);
if (index === -1) {
index = element.renderer.bindingGroups.length;
element.renderer.bindingGroups.push(keypath);
}
this.bindingGroup = index;
}
fromDom() {
return `${this.element.var}.__value`;
}
toDom() {
const condition = `${this.element.var}.__value === ${this.binding.value.snippet}`;
return `${this.element.var}.checked = ${condition};`
}
render(block: Block) {
block.builders.hydrate.addLine(
`#component._bindingGroups[${this.bindingGroup}].push(${this.element.var});`
);
block.builders.destroy.addLine(
`#component._bindingGroups[${this.bindingGroup}].splice(#component._bindingGroups[${this.bindingGroup}].indexOf(${this.element.var}), 1);`
);
super.render(block);
}
}

@ -1,30 +0,0 @@
import InputNumberBinding from './InputNumberBinding';
import Binding from '../../../../nodes/Binding';
import Element from '../../../../nodes/Element';
import Block from '../../../Block';
import ElementWrapper from '..';
export default class InputRangeBinding extends InputNumberBinding {
events = ['change', 'input'];
static filter(
node: Element,
binding_lookup: Record<string, Binding>,
type: string
) {
return (
node.name === 'input' &&
type === 'range' &&
binding_lookup.value
);
}
constructor(
block: Block,
element: ElementWrapper,
binding_lookup: Record<string, Binding>
) {
super(block, element, binding_lookup);
this.needsLock = false;
}
}

@ -1,40 +0,0 @@
import Binding from '../../../../nodes/Binding';
import Element from '../../../../nodes/Element';
import ElementWrapper from '..';
import BindingWrapper from './Binding';
import Block from '../../../Block';
export default class InputTextBinding extends BindingWrapper {
events = ['input'];
static filter(
node: Element,
binding_lookup: Record<string, Binding>,
type: string
) {
if (node.name === 'textarea' && binding_lookup.value) {
return true;
}
if (node.name === 'input' && binding_lookup.value) {
return !/radio|checkbox|range/.test(type);
}
}
constructor(
block: Block,
element: ElementWrapper,
binding_lookup: Record<string, Binding>
) {
super(block, element, binding_lookup.value);
this.needsLock = true;
}
fromDom() {
return `${this.element.var}.value`;
}
toDom() {
return `${this.element.var}.value = ${this.binding.value.snippet};`;
}
}

@ -1,59 +0,0 @@
import Binding from '../../../../nodes/Binding';
import Element from '../../../../nodes/Element';
import ElementWrapper from '..';
import BindingWrapper from './Binding';
import Block from '../../../Block';
export default class SelectBinding extends BindingWrapper {
element: ElementWrapper;
node: Binding;
static filter(
node: Element,
binding_lookup: Record<string, Binding>,
type: string
) {
return node.name === 'select' && binding_lookup.value;
}
constructor(
block: Block,
element: ElementWrapper,
binding_lookup: Record<string, Binding>
) {
super(block, element, binding_lookup.value);
this.events = ['change'];
this.needsLock = true;
element.renderer.hasComplexBindings = true;
// TODO does this also apply to e.g. `<input type='checkbox' bind:group='foo'>`?
const dependencies = this.binding.value.dependencies;
this.element.selectBindingDependencies = dependencies;
dependencies.forEach((prop: string) => {
element.renderer.component.indirectDependencies.set(prop, new Set());
});
}
fromDom() {
return this.element.node.getStaticAttributeValue('multiple') === true ?
`@selectMultipleValue(${this.element.var})` :
`@selectValue(${this.element.var})`;
}
toDom() {
return this.element.getStaticAttributeValue('multiple') === true ?
`@selectOptions(${this.element.var}, ${this.binding.value.snippet});` :
`@selectOption(${this.element.var}, ${this.binding.value.snippet});`;
}
render(block: Block) {
super.render(block);
const allInitialStateIsDefined = `'${this.object}' in ctx`;
block.builders.hydrate.addLine(
`if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${this.handlerName});`
);
}
}

@ -14,26 +14,8 @@ import namespaces from '../../../../utils/namespaces';
import AttributeWrapper from './Attribute';
import StyleAttributeWrapper from './StyleAttribute';
import { dimensions } from '../../../../utils/patterns';
import Binding from './UNUSED-Binding';
// import InputCheckboxBinding from './Binding/InputCheckboxBinding';
// import InputCheckboxGroupBinding from './Binding/InputCheckboxGroupBinding';
// import InputNumberBinding from './Binding/InputNumberBinding';
// import InputRangeBinding from './Binding/InputRangeBinding';
// import InputTextBinding from './Binding/InputTextBinding';
// import InputRadioGroupBinding from './Binding/InputRadioGroupBinding';
// import SelectBinding from './Binding/SelectBinding';
import Binding from './Binding';
import InlineComponentWrapper from '../InlineComponent';
import addToSet from '../../../../utils/addToSet';
// const bindings = [
// InputTextBinding,
// InputRadioGroupBinding,
// InputCheckboxBinding,
// InputCheckboxGroupBinding,
// InputNumberBinding,
// InputRangeBinding,
// SelectBinding
// ];
const events = [
{

Loading…
Cancel
Save