some progress

pull/1746/head
Rich Harris 7 years ago
parent 2dce34c53c
commit e55a806a52

@ -138,7 +138,6 @@ export default class Component {
code: MagicString;
bindingGroups: string[];
indirectDependencies: Map<string, Set<string>>;
expectedProperties: Set<string>;
refs: Set<string>;
@ -197,7 +196,6 @@ export default class Component {
this.refs = new Set();
this.refCallees = [];
this.bindingGroups = [];
this.indirectDependencies = new Map();
this.file = options.filename && (

@ -7,16 +7,6 @@ import createDebuggingComment from '../../utils/createDebuggingComment';
import Expression from './shared/Expression';
import mapChildren from './shared/mapChildren';
function isElseIf(node: ElseBlock) {
return (
node && node.children.length === 1 && node.children[0].type === 'IfBlock'
);
}
function isElseBranch(branch) {
return branch.block && !branch.condition;
}
export default class IfBlock extends Node {
type: 'IfBlock';
expression: Expression;
@ -38,82 +28,6 @@ export default class IfBlock extends Node {
this.warnIfEmptyBlock();
}
init(
block: Block,
stripWhitespace: boolean,
nextSibling: Node
) {
const { component } = this;
this.cannotUseInnerHTML();
const blocks: Block[] = [];
let dynamic = false;
let hasIntros = false;
let hasOutros = false;
function attachBlocks(node: IfBlock) {
node.var = block.getUniqueName(`if_block`);
block.addDependencies(node.expression.dependencies);
node.block = block.child({
comment: createDebuggingComment(node, component),
name: component.getUniqueName(`create_if_block`),
});
blocks.push(node.block);
node.initChildren(node.block, stripWhitespace, nextSibling);
if (node.block.dependencies.size > 0) {
dynamic = true;
block.addDependencies(node.block.dependencies);
}
if (node.block.hasIntros) hasIntros = true;
if (node.block.hasOutros) hasOutros = true;
if (isElseIf(node.else)) {
attachBlocks(node.else.children[0]);
} else if (node.else) {
node.else.block = block.child({
comment: createDebuggingComment(node.else, component),
name: component.getUniqueName(`create_if_block`),
});
blocks.push(node.else.block);
node.else.initChildren(
node.else.block,
stripWhitespace,
nextSibling
);
if (node.else.block.dependencies.size > 0) {
dynamic = true;
block.addDependencies(node.else.block.dependencies);
}
if (node.else.block.hasIntros) hasIntros = true;
if (node.else.block.hasOutros) hasOutros = true;
}
}
attachBlocks(this);
if (component.options.nestedTransitions) {
if (hasIntros) block.addIntro();
if (hasOutros) block.addOutro();
}
blocks.forEach(block => {
block.hasUpdateMethod = dynamic;
block.hasIntroMethod = hasIntros;
block.hasOutroMethod = hasOutros;
});
component.target.blocks.push(...blocks);
}
build(
block: Block,
parentNode: string,

@ -11,6 +11,7 @@ export default class Renderer {
readonly: Set<string>;
slots: Set<string>;
metaBindings: string[];
bindingGroups: string[];
block: Block;
fragment: FragmentWrapper;
@ -35,6 +36,8 @@ export default class Renderer {
// initial values for e.g. window.innerWidth, if there's a <svelte:window> meta tag
this.metaBindings = [];
this.bindingGroups = [];
// main block
this.block = new Block({
component,

@ -166,8 +166,8 @@ export default function dom(
return `if (${conditions.join(' && ')}) console.warn("${message}");`
})}
${component.bindingGroups.length &&
`this._bindingGroups = [${Array(component.bindingGroups.length).fill('[]').join(', ')}];`}
${renderer.bindingGroups.length &&
`this._bindingGroups = [${Array(renderer.bindingGroups.length).fill('[]').join(', ')}];`}
this._intro = ${component.options.skipIntroByDefault ? '!!options.intro' : 'true'};
${templateProperties.onstate && `this._handlers.state = [%onstate];`}

@ -1,290 +0,0 @@
import Binding from '../../../nodes/Binding';
import ElementWrapper from '.';
import { dimensions } from '../../../../utils/patterns';
import getObject from '../../../../utils/getObject';
import Block from '../../Block';
type Handler = {
}
const readOnlyMediaAttributes = new Set([
'duration',
'buffered',
'seekable',
'played'
]);
export default class BindingWrapper {
node: Binding;
parent: ElementWrapper;
object: string;
handler: Handler;
updateDom: string;
initialUpdate: string;
needsLock: boolean;
updateCondition: string;
isReadOnlyMediaAttribute: boolean;
constructor(node: Binding, parent: ElementWrapper) {
this.node = node;
this.parent = parent;
parent.cannotUseInnerHTML();
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)
);
let updateConditions: string[] = [];
const { name } = getObject(this.node.value.node);
const { snippet } = this.node.value;
// 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?
const dependencies = new Set(this.node.value.dependencies);
this.node.value.dependencies.forEach((prop: string) => {
const indirectDependencies = parent.renderer.component.indirectDependencies.get(prop);
if (indirectDependencies) {
indirectDependencies.forEach(indirectDependency => {
dependencies.add(indirectDependency);
});
}
});
// view to model
const valueFromDom = getValueFromDom(this.component, parent.node, this);
const handler = getEventHandler(this, this.component, block, name, snippet, dependencies, valueFromDom);
// model to view
let updateDom = getDomUpdater(node, this, snippet);
let initialUpdate = updateDom;
// special cases
if (this.node.name === 'group') {
const bindingGroup = getBindingGroup(this.component, this.value.node);
block.builders.hydrate.addLine(
`#component._bindingGroups[${bindingGroup}].push(${node.var});`
);
block.builders.destroy.addLine(
`#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${node.var}), 1);`
);
}
if (this.node.name === 'currentTime' || this.node.name === 'volume') {
updateConditions.push(`!isNaN(${snippet})`);
if (this.node.name === 'currentTime') initialUpdate = null;
}
if (this.node.name === 'paused') {
// this is necessary to prevent audio restarting by itself
const last = block.getUniqueName(`${node.var}_is_paused`);
block.addVariable(last, 'true');
updateConditions.push(`${last} !== (${last} = ${snippet})`);
updateDom = `${node.var}[${last} ? "pause" : "play"]();`;
initialUpdate = null;
}
// bind:offsetWidth and bind:offsetHeight
if (dimensions.test(this.node.name)) {
initialUpdate = null;
updateDom = null;
}
const dependencyArray = [...this.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 = !isReadOnly && needsLock;
this.updateCondition = updateConditions.length ? updateConditions.join(' && ') : undefined;
this.isReadOnlyMediaAttribute = readOnlyMediaAttributes.has(this.node.name);
}
render(block: Block) {
}
}
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};`;
}
function getBindingGroup(component: Component, 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 = component.bindingGroups.indexOf(keypath);
if (index === -1) {
index = component.bindingGroups.length;
component.bindingGroups.push(keypath);
}
return index;
}
function getEventHandler(
binding: Binding,
component: Component,
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 => !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 getValueFromDom(
component: Component,
node: Element,
binding: Node
) {
// <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(component, 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 isComputed(node: Node) {
while (node.type === 'MemberExpression') {
if (node.computed) return true;
node = node.object;
}
return false;
}

@ -0,0 +1,360 @@
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 { Node } from '../../../../../interfaces';
import Element from '../../../../nodes/Element';
import flattenReference from '../../../../../utils/flattenReference';
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[];
usesStore: boolean;
needsLock: boolean;
lock: string;
mutations: string[];
props: Set<string>;
storeProps: Set<string>;
object: string;
// handler: Handler;
updateDom: string;
initialUpdate: string;
updateCondition: string;
isReadOnly: boolean;
isReadOnlyMediaAttribute: boolean;
constructor(element: ElementWrapper, binding: Binding) {
this.element = element;
this.binding = binding;
element.cannotUseInnerHTML();
this.isReadOnly = false;
this.needsLock = false;
this.events = [];
}
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 false;
}
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;
// 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?
const dependencies = new Set(this.binding.value.dependencies);
this.binding.value.dependencies.forEach((prop: string) => {
const indirectDependencies = this.element.renderer.component.indirectDependencies.get(prop);
if (indirectDependencies) {
indirectDependencies.forEach(indirectDependency => {
dependencies.add(indirectDependency);
});
}
});
// view to model
const valueFromDom = this.fromDom();
const handler = getEventHandler(this.binding, this.element.renderer, block, name, snippet, 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;
this.isReadOnlyMediaAttribute = readOnlyMediaAttributes.has(this.binding.name);
let animation_frame = null; // TODO media binding only
const handler_name = block.getUniqueName(`${this.element.var}_${this.events.join('_')}_handler`);
block.builders.init.addBlock(deindent`
function ${handler_name}() {
${
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
);
block.builders.hydrate.addLine(
`@addListener(${this.element.var}, "${name}", ${handler_name});`
);
block.builders.destroy.addLine(
`@removeListener(${this.element.var}, "${name}", ${handler_name});`
);
}
}
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};`;
}

@ -0,0 +1,84 @@
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;
static filter(
node: Element,
binding_lookup: Record<string, Binding>,
type: string
) {
if (node.name === 'input' && binding_lookup.group) {
return type === 'radio';
}
}
constructor(
element: ElementWrapper,
binding_lookup: Record<string, Binding>
) {
super(element, binding_lookup.group);
this.events = ['change'];
}
fromDom() {
const bindingGroup = getBindingGroup(this.element.renderer, this.binding.value.node);
if (this.element.node.getStaticAttributeValue('type') === 'checkbox') {
return `@getBindingGroupValue(#component._bindingGroups[${bindingGroup}])`;
}
return `${this.element.var}.__value`;
}
toDom() {
const type = this.element.node.getStaticAttributeValue('type');
const condition = type === 'checkbox'
? `~${this.binding.value.snippet}.indexOf(${this.element.var}.__value)`
: `${this.element.var}.__value === ${this.binding.value.snippet}`;
return `${this.element.var}.checked = ${condition};`
}
render(block: Block) {
const bindingGroup = getBindingGroup(
this.element.renderer,
this.binding.value.node
);
block.builders.hydrate.addLine(
`#component._bindingGroups[${bindingGroup}].push(${this.element.var});`
);
block.builders.destroy.addLine(
`#component._bindingGroups[${bindingGroup}].splice(#component._bindingGroups[${bindingGroup}].indexOf(${this.element.var}), 1);`
);
super.render(block);
// this.renderHandler(block, 'TODO');
}
}
function getBindingGroup(renderer: Renderer, 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 = renderer.bindingGroups.indexOf(keypath);
if (index === -1) {
index = renderer.bindingGroups.length;
renderer.bindingGroups.push(keypath);
}
return index;
}

@ -0,0 +1,32 @@
import Binding from '../../../../nodes/Binding';
import Element from '../../../../nodes/Element';
import ElementWrapper from '..';
import Block from '../../../Block';
import BindingWrapper from './Binding';
export default class InputTextBinding extends BindingWrapper {
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(
element: ElementWrapper,
binding_lookup: Record<string, Binding>
) {
super(element);
}
render(block: Block) {
}
}

@ -0,0 +1,120 @@
import Binding from '../../../nodes/Binding';
import ElementWrapper from '.';
import { dimensions } from '../../../../utils/patterns';
import getObject from '../../../../utils/getObject';
import Block from '../../Block';
type Handler = {
}
export default class BindingWrapper {
node: Binding;
parent: ElementWrapper;
object: string;
handler: Handler;
updateDom: string;
initialUpdate: string;
needsLock: boolean;
updateCondition: string;
isReadOnlyMediaAttribute: boolean;
constructor(node: Binding, parent: ElementWrapper) {
this.node = node;
this.parent = parent;
parent.cannotUseInnerHTML();
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)
);
let updateConditions: string[] = [];
const { name } = getObject(this.node.value.node);
const { snippet } = this.node.value;
// 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?
const dependencies = new Set(this.node.value.dependencies);
this.node.value.dependencies.forEach((prop: string) => {
const indirectDependencies = parent.renderer.component.indirectDependencies.get(prop);
if (indirectDependencies) {
indirectDependencies.forEach(indirectDependency => {
dependencies.add(indirectDependency);
});
}
});
// view to model
const valueFromDom = getValueFromDom(this.component, parent.node, this);
const handler = getEventHandler(this, this.component, block, name, snippet, dependencies, valueFromDom);
// model to view
let updateDom = getDomUpdater(node, this, snippet);
let initialUpdate = updateDom;
if (this.node.name === 'currentTime' || this.node.name === 'volume') {
updateConditions.push(`!isNaN(${snippet})`);
if (this.node.name === 'currentTime') initialUpdate = null;
}
if (this.node.name === 'paused') {
// this is necessary to prevent audio restarting by itself
const last = block.getUniqueName(`${node.var}_is_paused`);
block.addVariable(last, 'true');
updateConditions.push(`${last} !== (${last} = ${snippet})`);
updateDom = `${node.var}[${last} ? "pause" : "play"]();`;
initialUpdate = null;
}
// bind:offsetWidth and bind:offsetHeight
if (dimensions.test(this.node.name)) {
initialUpdate = null;
updateDom = null;
}
const dependencyArray = [...this.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 = !isReadOnly && needsLock;
this.updateCondition = updateConditions.length ? updateConditions.join(' && ') : undefined;
this.isReadOnlyMediaAttribute = readOnlyMediaAttributes.has(this.node.name);
}
render(block: Block) {
}
}
function isComputed(node: Node) {
while (node.type === 'MemberExpression') {
if (node.computed) return true;
node = node.object;
}
return false;
}

@ -15,7 +15,13 @@ import namespaces from '../../../../utils/namespaces';
import AttributeWrapper from './Attribute';
import StyleAttributeWrapper from './StyleAttribute';
import { dimensions } from '../../../../utils/patterns';
import BindingWrapper from './Binding';
import InputTextBinding from './Binding/InputTextBinding';
import InputRadioGroupBinding from './Binding/InputRadioGroupBinding';
const bindings = [
InputTextBinding,
InputRadioGroupBinding
];
const events = [
{
@ -85,7 +91,10 @@ export default class ElementWrapper extends Wrapper {
node: Element;
fragment: FragmentWrapper;
attributes: AttributeWrapper[];
bindings: BindingWrapper[];
bindings: Array<
InputTextBinding |
InputRadioGroupBinding
>;
classDependencies: string[];
initialUpdate: string;
@ -111,10 +120,22 @@ export default class ElementWrapper extends Wrapper {
return new AttributeWrapper(attribute, this);
});
this.bindings = this.node.bindings.map(binding => {
return new BindingWrapper(binding, this);
const binding_lookup = {};
this.node.bindings.forEach(binding => {
binding_lookup[binding.name] = binding;
});
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 = bindings
.filter(Binding => {
return Binding.filter(this.node, binding_lookup, type);
})
.map(Binding => new Binding(this, binding_lookup));
this.fragment = new FragmentWrapper(renderer, block, node.children, this, stripWhitespace, nextSibling);
}
@ -203,7 +224,7 @@ export default class ElementWrapper extends Wrapper {
);
const eventHandlerOrBindingUsesContext = (
this.bindings.some(binding => binding.node.usesContext) ||
this.bindings.some(binding => binding.usesContext) ||
this.node.handlers.some(handler => handler.usesContext)
);
@ -318,127 +339,145 @@ export default class ElementWrapper extends Wrapper {
this.renderer.hasComplexBindings = true;
}
const needsLock = this.node.name !== 'input' || !/radio|checkbox|range|color/.test(this.getStaticAttributeValue('type'));
const lock = this.bindings.some(binding => binding.needsLock) ?
block.getUniqueName(`${this.var}_updating`) :
null;
if (lock) block.addVariable(lock, 'false');
const groups = events
.map(event => {
return {
events: event.eventNames,
bindings: this.bindings.filter(binding => event.filter(this, binding.name))
};
})
.filter(group => group.bindings.length);
groups.forEach(group => {
const handler = block.getUniqueName(`${this.var}_${group.events.join('_')}_handler`);
const needsLock = group.bindings.some(binding => binding.needsLock);
group.bindings.forEach(binding => {
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
);
});
const usesStore = group.bindings.some(binding => binding.handler.usesStore);
const mutations = group.bindings.map(binding => binding.handler.mutation).filter(Boolean).join('\n');
const props = new Set();
const storeProps = new Set();
group.bindings.forEach(binding => {
binding.handler.props.forEach(prop => {
props.add(prop);
});
binding.handler.storeProps.forEach(prop => {
storeProps.add(prop);
});
}); // TODO use stringifyProps here, once indenting is fixed
// media bindings — awkward special case. The native timeupdate events
// fire too infrequently, so we need to take matters into our
// own hands
let animation_frame;
if (group.events[0] === 'timeupdate') {
animation_frame = block.getUniqueName(`${this.var}_animationframe`);
block.addVariable(animation_frame);
}
block.builders.init.addBlock(deindent`
function ${handler}() {
${
animation_frame && deindent`
cancelAnimationFrame(${animation_frame});
if (!${this.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`
}
${usesStore && `var $ = #component.store.get();`}
${needsLock && `${lock} = true;`}
${mutations.length > 0 && mutations}
${props.size > 0 && `#component.set({ ${Array.from(props).join(', ')} });`}
${storeProps.size > 0 && `#component.store.set({ ${Array.from(storeProps).join(', ')} });`}
${needsLock && `${lock} = false;`}
}
`);
group.events.forEach(name => {
if (name === 'resize') {
// special case
const resize_listener = block.getUniqueName(`${this.var}_resize_listener`);
block.addVariable(resize_listener);
block.builders.mount.addLine(
`${resize_listener} = @addResizeListener(${this.var}, ${handler});`
);
block.builders.destroy.addLine(
`${resize_listener}.cancel();`
);
} else {
block.builders.hydrate.addLine(
`@addListener(${this.var}, "${name}", ${handler});`
);
block.builders.destroy.addLine(
`@removeListener(${this.var}, "${name}", ${handler});`
);
}
});
const allInitialStateIsDefined = group.bindings
.map(binding => `'${binding.object}' in ctx`)
.join(' && ');
if (this.name === 'select' || group.bindings.find(binding => binding.name === 'indeterminate' || binding.isReadOnlyMediaAttribute)) {
this.component.target.hasComplexBindings = true;
block.builders.hydrate.addLine(
`if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${handler});`
);
}
if (group.events[0] === 'resize') {
this.component.target.hasComplexBindings = true;
block.builders.hydrate.addLine(
`#component.root._beforecreate.push(${handler});`
);
}
this.bindings.forEach(binding => {
binding.render(block);
});
this.initialUpdate = this.bindings.map(binding => binding.initialUpdate).filter(Boolean).join('\n');
}
// addBindings(block: Block) {
// if (this.bindings.length === 0) return;
// if (this.node.name === 'select' || this.isMediaNode()) {
// this.renderer.hasComplexBindings = true;
// }
// const needsLock = this.node.name !== 'input' || !/radio|checkbox|range|color/.test(this.getStaticAttributeValue('type'));
// const lock = this.bindings.some(binding => binding.needsLock) ?
// block.getUniqueName(`${this.var}_updating`) :
// null;
// if (lock) block.addVariable(lock, 'false');
// const groups = events
// .map(event => {
// return {
// events: event.eventNames,
// bindings: this.bindings.filter(binding => event.filter(this, binding.name))
// };
// })
// .filter(group => group.bindings.length);
// if (groups.length > 1) {
// throw new Error('huh');
// }
// groups.forEach(group => {
// const handler = block.getUniqueName(`${this.var}_${group.events.join('_')}_handler`);
// const needsLock = group.bindings.some(binding => binding.needsLock);
// group.bindings.forEach(binding => {
// 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
// );
// });
// const usesStore = group.bindings.some(binding => binding.handler.usesStore);
// const mutations = group.bindings.map(binding => binding.handler.mutation).filter(Boolean).join('\n');
// const props = new Set();
// const storeProps = new Set();
// group.bindings.forEach(binding => {
// binding.handler.props.forEach(prop => {
// props.add(prop);
// });
// binding.handler.storeProps.forEach(prop => {
// storeProps.add(prop);
// });
// }); // TODO use stringifyProps here, once indenting is fixed
// // media bindings — awkward special case. The native timeupdate events
// // fire too infrequently, so we need to take matters into our
// // own hands
// let animation_frame;
// if (group.events[0] === 'timeupdate') {
// animation_frame = block.getUniqueName(`${this.var}_animationframe`);
// block.addVariable(animation_frame);
// }
// block.builders.init.addBlock(deindent`
// function ${handler}() {
// ${
// animation_frame && deindent`
// cancelAnimationFrame(${animation_frame});
// if (!${this.var}.paused) ${animation_frame} = requestAnimationFrame(${handler});`
// }
// ${usesStore && `var $ = #component.store.get();`}
// ${needsLock && `${lock} = true;`}
// ${mutations.length > 0 && mutations}
// ${props.size > 0 && `#component.set({ ${Array.from(props).join(', ')} });`}
// ${storeProps.size > 0 && `#component.store.set({ ${Array.from(storeProps).join(', ')} });`}
// ${needsLock && `${lock} = false;`}
// }
// `);
// group.events.forEach(name => {
// if (name === 'resize') {
// // special case
// const resize_listener = block.getUniqueName(`${this.var}_resize_listener`);
// block.addVariable(resize_listener);
// block.builders.mount.addLine(
// `${resize_listener} = @addResizeListener(${this.var}, ${handler});`
// );
// block.builders.destroy.addLine(
// `${resize_listener}.cancel();`
// );
// } else {
// block.builders.hydrate.addLine(
// `@addListener(${this.var}, "${name}", ${handler});`
// );
// block.builders.destroy.addLine(
// `@removeListener(${this.var}, "${name}", ${handler});`
// );
// }
// });
// const allInitialStateIsDefined = group.bindings
// .map(binding => `'${binding.object}' in ctx`)
// .join(' && ');
// if (this.name === 'select' || group.bindings.find(binding => binding.name === 'indeterminate' || binding.isReadOnlyMediaAttribute)) {
// this.component.target.hasComplexBindings = true;
// block.builders.hydrate.addLine(
// `if (!(${allInitialStateIsDefined})) #component.root._beforecreate.push(${handler});`
// );
// }
// if (group.events[0] === 'resize') {
// this.component.target.hasComplexBindings = true;
// block.builders.hydrate.addLine(
// `#component.root._beforecreate.push(${handler});`
// );
// }
// });
// this.initialUpdate = this.bindings.map(binding => binding.initialUpdate).filter(Boolean).join('\n');
// }
addAttributes(block: Block) {
if (this.node.attributes.find(attr => attr.type === 'Spread')) {
this.addSpreadAttributes(block);
@ -574,7 +613,7 @@ export default class ElementWrapper extends Wrapper {
}
addRef(block: Block) {
const ref = `#component.refs.${this.ref.name}`;
const ref = `#component.refs.${this.node.ref.name}`;
block.builders.mount.addLine(
`${ref} = ${this.var};`

@ -1,6 +1,7 @@
import Wrapper from './shared/wrapper';
import EachBlock from './EachBlock';
import Element from './Element';
import IfBlock from './IfBlock';
import InlineComponent from './InlineComponent';
import MustacheTag from './MustacheTag';
import Text from './Text';
@ -15,6 +16,7 @@ const wrappers = {
Comment: null,
EachBlock,
Element,
IfBlock,
InlineComponent,
MustacheTag,
Text,

@ -0,0 +1,476 @@
import Wrapper from './shared/wrapper';
import Renderer from '../Renderer';
import Block from '../Block';
import EachBlock from '../../nodes/EachBlock';
import IfBlock from '../../nodes/IfBlock';
import createDebuggingComment from '../../../utils/createDebuggingComment';
import ElseBlock from '../../nodes/ElseBlock';
import FragmentWrapper from './Fragment';
import deindent from '../../../utils/deindent';
function isElseIf(node: ElseBlock) {
return (
node && node.children.length === 1 && node.children[0].type === 'IfBlock'
);
}
function isElseBranch(branch) {
return branch.block && !branch.condition;
}
class IfBlockBranch extends Wrapper {
block: Block;
fragment: FragmentWrapper;
condition: string;
isDynamic: boolean;
var = null;
constructor(
renderer: Renderer,
block: Block,
parent: IfBlockWrapper,
node: IfBlock | ElseBlock,
stripWhitespace: boolean,
nextSibling: Wrapper
) {
super(renderer, block, parent, node);
this.condition = (<IfBlock>node).expression && (<IfBlock>node).expression.snippet;
this.block = block.child({
comment: createDebuggingComment(node, parent.renderer.component),
name: parent.renderer.component.getUniqueName(
(<IfBlock>node).expression ? `create_if_block` : `create_else_block`
)
});
this.fragment = new FragmentWrapper(renderer, block, node.children, parent.parent, stripWhitespace, nextSibling);
this.isDynamic = this.block.dependencies.size > 0;
}
}
export default class IfBlockWrapper extends Wrapper {
node: IfBlock;
branches: IfBlockBranch[];
var = 'if_block';
constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: EachBlock,
stripWhitespace: boolean,
nextSibling: Wrapper
) {
super(renderer, block, parent, node);
const { component } = renderer;
this.cannotUseInnerHTML();
this.branches = [];
const blocks: Block[] = [];
let dynamic = false;
let hasIntros = false;
let hasOutros = false;
const createBranches = (node: IfBlock) => {
const branch = new IfBlockBranch(
renderer,
block,
this,
node,
stripWhitespace,
nextSibling
);
this.branches.push(branch);
blocks.push(branch.block);
block.addDependencies(node.expression.dependencies);
if (branch.isDynamic) {
dynamic = true;
block.addDependencies(branch.block.dependencies);
}
if (branch.block.hasIntros) hasIntros = true;
if (branch.block.hasOutros) hasOutros = true;
if (isElseIf(node.else)) {
createBranches(node.else.children[0]);
} else if (node.else) {
const branch = new IfBlockBranch(
renderer,
block,
this,
node.else,
stripWhitespace,
nextSibling
);
this.branches.push(branch);
blocks.push(branch.block);
if (branch.block.dependencies.size > 0) {
dynamic = true;
block.addDependencies(branch.block.dependencies);
}
if (branch.block.hasIntros) hasIntros = true;
if (branch.block.hasOutros) hasOutros = true;
}
};
createBranches(this.node);
if (component.options.nestedTransitions) {
if (hasIntros) block.addIntro();
if (hasOutros) block.addOutro();
}
blocks.forEach(block => {
block.hasUpdateMethod = dynamic;
block.hasIntroMethod = hasIntros;
block.hasOutroMethod = hasOutros;
});
renderer.blocks.push(...blocks);
}
render(
block: Block,
parentNode: string,
parentNodes: string
) {
const name = this.var;
const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode();
const anchor = needsAnchor
? block.getUniqueName(`${name}_anchor`)
: (this.next && this.next.var) || 'null';
const hasElse = !(this.branches[this.branches.length - 1].condition);
const if_name = hasElse ? '' : `if (${name}) `;
const dynamic = this.branches[0].block.hasUpdateMethod; // can use [0] as proxy for all, since they necessarily have the same value
const hasOutros = this.branches[0].block.hasOutroMethod;
const vars = { name, anchor, if_name, hasElse };
if (this.node.else) {
if (hasOutros) {
this.renderCompoundWithOutros(block, parentNode, parentNodes, branches, dynamic, vars);
if (this.renderer.options.nestedTransitions) {
block.builders.outro.addBlock(deindent`
if (${name}) ${name}.o(#outrocallback);
else #outrocallback();
`);
}
} else {
this.renderCompound(block, parentNode, parentNodes, dynamic, vars);
}
} else {
this.renderSimple(block, parentNode, parentNodes, dynamic, vars);
if (hasOutros && this.renderer.options.nestedTransitions) {
block.builders.outro.addBlock(deindent`
if (${name}) ${name}.o(#outrocallback);
else #outrocallback();
`);
}
}
block.builders.create.addLine(`${if_name}${name}.c();`);
if (parentNodes) {
block.builders.claim.addLine(
`${if_name}${name}.l(${parentNodes});`
);
}
if (needsAnchor) {
block.addElement(
anchor,
`@createComment()`,
parentNodes && `@createComment()`,
parentNode
);
}
this.branches.forEach(branch => {
branch.fragment.render(branch.block, parentNode, parentNodes);
});
}
renderCompound(
block: Block,
parentNode: string,
parentNodes: string,
dynamic,
{ name, anchor, hasElse, if_name }
) {
const select_block_type = this.component.getUniqueName(`select_block_type`);
const current_block_type = block.getUniqueName(`current_block_type`);
const current_block_type_and = hasElse ? '' : `${current_block_type} && `;
block.builders.init.addBlock(deindent`
function ${select_block_type}(ctx) {
${this.branches
.map(({ condition, block }) => `${condition ? `if (${condition}) ` : ''}return ${block.name};`)
.join('\n')}
}
`);
block.builders.init.addBlock(deindent`
var ${current_block_type} = ${select_block_type}(ctx);
var ${name} = ${current_block_type_and}${current_block_type}(#component, ctx);
`);
const mountOrIntro = this.branches[0].block.hasIntroMethod ? 'i' : 'm';
const initialMountNode = parentNode || '#target';
const anchorNode = parentNode ? 'null' : 'anchor';
block.builders.mount.addLine(
`${if_name}${name}.${mountOrIntro}(${initialMountNode}, ${anchorNode});`
);
const updateMountNode = this.getUpdateMountNode(anchor);
const changeBlock = deindent`
${if_name}${name}.d(1);
${name} = ${current_block_type_and}${current_block_type}(#component, ctx);
${if_name}${name}.c();
${if_name}${name}.${mountOrIntro}(${updateMountNode}, ${anchor});
`;
if (dynamic) {
block.builders.update.addBlock(deindent`
if (${current_block_type} === (${current_block_type} = ${select_block_type}(ctx)) && ${name}) {
${name}.p(changed, ctx);
} else {
${changeBlock}
}
`);
} else {
block.builders.update.addBlock(deindent`
if (${current_block_type} !== (${current_block_type} = ${select_block_type}(ctx))) {
${changeBlock}
}
`);
}
block.builders.destroy.addLine(`${if_name}${name}.d(${parentNode ? '' : 'detach'});`);
}
// if any of the siblings have outros, we need to keep references to the blocks
// (TODO does this only apply to bidi transitions?)
renderCompoundWithOutros(
block: Block,
parentNode: string,
parentNodes: string,
dynamic,
{ name, anchor, hasElse }
) {
const select_block_type = this.renderer.component.getUniqueName(`select_block_type`);
const current_block_type_index = block.getUniqueName(`current_block_type_index`);
const previous_block_index = block.getUniqueName(`previous_block_index`);
const if_block_creators = block.getUniqueName(`if_block_creators`);
const if_blocks = block.getUniqueName(`if_blocks`);
const if_current_block_type_index = hasElse
? ''
: `if (~${current_block_type_index}) `;
block.addVariable(current_block_type_index);
block.addVariable(name);
block.builders.init.addBlock(deindent`
var ${if_block_creators} = [
${this.branches.map(branch => branch.block.name).join(',\n')}
];
var ${if_blocks} = [];
function ${select_block_type}(ctx) {
${this.branches
.map(({ condition, block }, i) => `${condition ? `if (${condition}) ` : ''}return ${block ? i : -1};`)
.join('\n')}
}
`);
if (hasElse) {
block.builders.init.addBlock(deindent`
${current_block_type_index} = ${select_block_type}(ctx);
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
`);
} else {
block.builders.init.addBlock(deindent`
if (~(${current_block_type_index} = ${select_block_type}(ctx))) {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
}
`);
}
const mountOrIntro = this.branches[0].block.hasIntroMethod ? 'i' : 'm';
const initialMountNode = parentNode || '#target';
const anchorNode = parentNode ? 'null' : 'anchor';
block.builders.mount.addLine(
`${if_current_block_type_index}${if_blocks}[${current_block_type_index}].${mountOrIntro}(${initialMountNode}, ${anchorNode});`
);
const updateMountNode = this.getUpdateMountNode(anchor);
const destroyOldBlock = deindent`
@groupOutros();
${name}.o(function() {
${if_blocks}[${previous_block_index}].d(1);
${if_blocks}[${previous_block_index}] = null;
});
`;
const createNewBlock = deindent`
${name} = ${if_blocks}[${current_block_type_index}];
if (!${name}) {
${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](#component, ctx);
${name}.c();
}
${name}.${mountOrIntro}(${updateMountNode}, ${anchor});
`;
const changeBlock = hasElse
? deindent`
${destroyOldBlock}
${createNewBlock}
`
: deindent`
if (${name}) {
${destroyOldBlock}
}
if (~${current_block_type_index}) {
${createNewBlock}
} else {
${name} = null;
}
`;
if (dynamic) {
block.builders.update.addBlock(deindent`
var ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(ctx);
if (${current_block_type_index} === ${previous_block_index}) {
${if_current_block_type_index}${if_blocks}[${current_block_type_index}].p(changed, ctx);
} else {
${changeBlock}
}
`);
} else {
block.builders.update.addBlock(deindent`
var ${previous_block_index} = ${current_block_type_index};
${current_block_type_index} = ${select_block_type}(ctx);
if (${current_block_type_index} !== ${previous_block_index}) {
${changeBlock}
}
`);
}
block.builders.destroy.addLine(deindent`
${if_current_block_type_index}${if_blocks}[${current_block_type_index}].d(${parentNode ? '' : 'detach'});
`);
}
renderSimple(
block: Block,
parentNode: string,
parentNodes: string,
dynamic,
{ name, anchor, if_name }
) {
const branch = this.branches[0];
block.builders.init.addBlock(deindent`
var ${name} = (${branch.condition}) && ${branch.block.name}(#component, ctx);
`);
const mountOrIntro = branch.block.hasIntroMethod ? 'i' : 'm';
const initialMountNode = parentNode || '#target';
const anchorNode = parentNode ? 'null' : 'anchor';
block.builders.mount.addLine(
`if (${name}) ${name}.${mountOrIntro}(${initialMountNode}, ${anchorNode});`
);
const updateMountNode = this.getUpdateMountNode(anchor);
const enter = dynamic
? (branch.block.hasIntroMethod || branch.block.hasOutroMethod)
? deindent`
if (${name}) {
${name}.p(changed, ctx);
} else {
${name} = ${branch.block.name}(#component, ctx);
if (${name}) ${name}.c();
}
${name}.i(${updateMountNode}, ${anchor});
`
: deindent`
if (${name}) {
${name}.p(changed, ctx);
} else {
${name} = ${branch.block.name}(#component, ctx);
${name}.c();
${name}.m(${updateMountNode}, ${anchor});
}
`
: (branch.block.hasIntroMethod || branch.block.hasOutroMethod)
? deindent`
if (!${name}) {
${name} = ${branch.block.name}(#component, ctx);
${name}.c();
}
${name}.i(${updateMountNode}, ${anchor});
`
: deindent`
if (!${name}) {
${name} = ${branch.block.name}(#component, ctx);
${name}.c();
${name}.m(${updateMountNode}, ${anchor});
}
`;
// no `p()` here — we don't want to update outroing nodes,
// as that will typically result in glitching
const exit = branch.block.hasOutroMethod
? deindent`
@groupOutros();
${name}.o(function() {
${name}.d(1);
${name} = null;
});
`
: deindent`
${name}.d(1);
${name} = null;
`;
block.builders.update.addBlock(deindent`
if (${branch.condition}) {
${enter}
} else if (${name}) {
${exit}
}
`);
block.builders.destroy.addLine(`${if_name}${name}.d(${parentNode ? '' : 'detach'});`);
}
}
Loading…
Cancel
Save