mirror of https://github.com/sveltejs/svelte
parent
3696e0865a
commit
a690ba0b8f
@ -1,296 +0,0 @@
|
||||
import Node from './shared/Node';
|
||||
import Element from './Element';
|
||||
import getObject from '../../utils/getObject';
|
||||
import getTailSnippet from '../../utils/getTailSnippet';
|
||||
import flattenReference from '../../utils/flattenReference';
|
||||
import Block from '../dom/Block';
|
||||
import Expression from './shared/Expression';
|
||||
import Compiler from '../../compile/Compiler';
|
||||
|
||||
const readOnlyMediaAttributes = new Set([
|
||||
'duration',
|
||||
'buffered',
|
||||
'seekable',
|
||||
'played'
|
||||
]);
|
||||
|
||||
export default class Binding extends Node {
|
||||
name: string;
|
||||
value: Expression;
|
||||
isContextual: boolean;
|
||||
usesContext: boolean;
|
||||
obj: string;
|
||||
prop: string;
|
||||
|
||||
constructor(compiler, parent, scope, info) {
|
||||
super(compiler, parent, scope, info);
|
||||
|
||||
this.name = info.name;
|
||||
this.value = new Expression(compiler, this, scope, info.value);
|
||||
|
||||
let obj;
|
||||
let prop;
|
||||
|
||||
const { name } = getObject(this.value.node);
|
||||
this.isContextual = scope.names.has(name);
|
||||
|
||||
if (this.value.node.type === 'MemberExpression') {
|
||||
prop = `[✂${this.value.node.property.start}-${this.value.node.property.end}✂]`;
|
||||
if (!this.value.node.computed) prop = `'${prop}'`;
|
||||
obj = `[✂${this.value.node.object.start}-${this.value.node.object.end}✂]`;
|
||||
|
||||
this.usesContext = true;
|
||||
} else {
|
||||
obj = 'ctx';
|
||||
prop = `'${name}'`;
|
||||
|
||||
this.usesContext = scope.names.has(name);
|
||||
}
|
||||
|
||||
this.obj = obj;
|
||||
this.prop = prop;
|
||||
}
|
||||
|
||||
munge(
|
||||
block: Block
|
||||
) {
|
||||
const node: Element = this.parent;
|
||||
|
||||
const needsLock = node.name !== 'input' || !/radio|checkbox|range|color/.test(node.getStaticAttributeValue('type'));
|
||||
const isReadOnly = node.isMediaNode() && readOnlyMediaAttributes.has(this.name);
|
||||
|
||||
let updateCondition: string;
|
||||
|
||||
const { name } = getObject(this.value.node);
|
||||
const { snippet } = this.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.value.dependencies);
|
||||
this.value.dependencies.forEach((prop: string) => {
|
||||
const indirectDependencies = this.compiler.indirectDependencies.get(prop);
|
||||
if (indirectDependencies) {
|
||||
indirectDependencies.forEach(indirectDependency => {
|
||||
dependencies.add(indirectDependency);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// view to model
|
||||
const valueFromDom = getValueFromDom(this.compiler, node, this);
|
||||
const handler = getEventHandler(this, this.compiler, block, name, snippet, dependencies, valueFromDom);
|
||||
|
||||
// model to view
|
||||
let updateDom = getDomUpdater(node, this, snippet);
|
||||
let initialUpdate = updateDom;
|
||||
|
||||
// special cases
|
||||
if (this.name === 'group') {
|
||||
const bindingGroup = getBindingGroup(this.compiler, 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.name === 'currentTime' || this.name === 'volume') {
|
||||
updateCondition = `!isNaN(${snippet})`;
|
||||
|
||||
if (this.name === 'currentTime')
|
||||
initialUpdate = null;
|
||||
}
|
||||
|
||||
if (this.name === 'paused') {
|
||||
// this is necessary to prevent audio restarting by itself
|
||||
const last = block.getUniqueName(`${node.var}_is_paused`);
|
||||
block.addVariable(last, 'true');
|
||||
|
||||
updateCondition = `${last} !== (${last} = ${snippet})`;
|
||||
updateDom = `${node.var}[${last} ? "pause" : "play"]();`;
|
||||
initialUpdate = null;
|
||||
}
|
||||
|
||||
return {
|
||||
name: this.name,
|
||||
object: name,
|
||||
handler,
|
||||
updateDom,
|
||||
initialUpdate,
|
||||
needsLock: !isReadOnly && needsLock,
|
||||
updateCondition,
|
||||
isReadOnlyMediaAttribute: this.isReadOnlyMediaAttribute()
|
||||
};
|
||||
}
|
||||
|
||||
isReadOnlyMediaAttribute() {
|
||||
return readOnlyMediaAttributes.has(this.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};`;
|
||||
}
|
||||
|
||||
function getBindingGroup(compiler: Compiler, 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 = compiler.bindingGroups.indexOf(keypath);
|
||||
if (index === -1) {
|
||||
index = compiler.bindingGroups.length;
|
||||
compiler.bindingGroups.push(keypath);
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
function getEventHandler(
|
||||
binding: Binding,
|
||||
compiler: Compiler,
|
||||
block: Block,
|
||||
name: string,
|
||||
snippet: string,
|
||||
dependencies: string[],
|
||||
value: string,
|
||||
isContextual: boolean
|
||||
) {
|
||||
const storeDependencies = [...dependencies].filter(prop => prop[0] === '$').map(prop => prop.slice(1));
|
||||
dependencies = [...dependencies].filter(prop => prop[0] !== '$');
|
||||
|
||||
if (binding.isContextual) {
|
||||
const tail = binding.value.node.type === 'MemberExpression'
|
||||
? getTailSnippet(binding.value.node)
|
||||
: '';
|
||||
|
||||
const list = `ctx.${block.listNames.get(name)}`;
|
||||
const index = `ctx.${block.indexNames.get(name)}`;
|
||||
|
||||
return {
|
||||
usesContext: true,
|
||||
usesState: true,
|
||||
usesStore: storeDependencies.length > 0,
|
||||
mutation: `${list}[${index}]${tail} = ${value};`,
|
||||
props: dependencies.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 `compiler.target.readonly` sooner so
|
||||
// that we don't have to do the `.some()` here
|
||||
dependencies = dependencies.filter(prop => !compiler.computations.some(computation => computation.key === prop));
|
||||
|
||||
return {
|
||||
usesContext: false,
|
||||
usesState: true,
|
||||
usesStore: storeDependencies.length > 0,
|
||||
mutation: `${snippet} = ${value}`,
|
||||
props: dependencies.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(
|
||||
compiler: Compiler,
|
||||
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(compiler, 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;
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
import Node from './shared/Node';
|
||||
import Compiler from '../Compiler';
|
||||
import mapChildren from './shared/mapChildren';
|
||||
import Block from '../dom/Block';
|
||||
import TemplateScope from './shared/TemplateScope';
|
||||
|
||||
export default class Fragment extends Node {
|
||||
block: Block;
|
||||
children: Node[];
|
||||
scope: TemplateScope;
|
||||
|
||||
constructor(compiler: Compiler, info: any) {
|
||||
const scope = new TemplateScope();
|
||||
super(compiler, null, scope, info);
|
||||
|
||||
this.scope = scope;
|
||||
this.children = mapChildren(compiler, this, scope, info.children);
|
||||
}
|
||||
|
||||
init() {
|
||||
this.block = new Block({
|
||||
compiler: this.compiler,
|
||||
name: '@create_main_fragment',
|
||||
key: null,
|
||||
|
||||
indexNames: new Map(),
|
||||
listNames: new Map(),
|
||||
|
||||
dependencies: new Set(),
|
||||
});
|
||||
|
||||
this.compiler.target.blocks.push(this.block);
|
||||
this.initChildren(this.block, true, null);
|
||||
|
||||
this.block.hasUpdateMethod = true;
|
||||
}
|
||||
|
||||
build() {
|
||||
this.init();
|
||||
|
||||
this.children.forEach(child => {
|
||||
child.build(this.block, null, 'nodes');
|
||||
});
|
||||
}
|
||||
}
|
@ -1,504 +0,0 @@
|
||||
import deindent from '../../utils/deindent';
|
||||
import Node from './shared/Node';
|
||||
import ElseBlock from './ElseBlock';
|
||||
import Block from '../dom/Block';
|
||||
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;
|
||||
children: any[];
|
||||
else: ElseBlock;
|
||||
|
||||
block: Block;
|
||||
|
||||
constructor(compiler, parent, scope, info) {
|
||||
super(compiler, parent, scope, info);
|
||||
|
||||
this.expression = new Expression(compiler, this, scope, info.expression);
|
||||
this.children = mapChildren(compiler, this, scope, info.children);
|
||||
|
||||
this.else = info.else
|
||||
? new ElseBlock(compiler, this, scope, info.else)
|
||||
: null;
|
||||
}
|
||||
|
||||
init(
|
||||
block: Block,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Node
|
||||
) {
|
||||
const { compiler } = 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, compiler),
|
||||
name: compiler.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.hasIntroMethod) hasIntros = true;
|
||||
if (node.block.hasOutroMethod) hasOutros = true;
|
||||
|
||||
if (isElseIf(node.else)) {
|
||||
attachBlocks(node.else.children[0]);
|
||||
} else if (node.else) {
|
||||
node.else.block = block.child({
|
||||
comment: createDebuggingComment(node.else, compiler),
|
||||
name: compiler.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attachBlocks(this);
|
||||
|
||||
blocks.forEach(block => {
|
||||
block.hasUpdateMethod = dynamic;
|
||||
block.hasIntroMethod = hasIntros;
|
||||
block.hasOutroMethod = hasOutros;
|
||||
});
|
||||
|
||||
compiler.target.blocks.push(...blocks);
|
||||
}
|
||||
|
||||
build(
|
||||
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 branches = this.getBranches(block, parentNode, parentNodes, this);
|
||||
|
||||
const hasElse = isElseBranch(branches[branches.length - 1]);
|
||||
const if_name = hasElse ? '' : `if (${name}) `;
|
||||
|
||||
const dynamic = branches[0].hasUpdateMethod; // can use [0] as proxy for all, since they necessarily have the same value
|
||||
const hasOutros = branches[0].hasOutroMethod;
|
||||
|
||||
const vars = { name, anchor, if_name, hasElse };
|
||||
|
||||
if (this.else) {
|
||||
if (hasOutros) {
|
||||
this.buildCompoundWithOutros(block, parentNode, parentNodes, branches, dynamic, vars);
|
||||
} else {
|
||||
this.buildCompound(block, parentNode, parentNodes, branches, dynamic, vars);
|
||||
}
|
||||
} else {
|
||||
this.buildSimple(block, parentNode, parentNodes, branches[0], dynamic, vars);
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
buildCompound(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string,
|
||||
branches,
|
||||
dynamic,
|
||||
{ name, anchor, hasElse, if_name }
|
||||
) {
|
||||
const select_block_type = this.compiler.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) {
|
||||
${branches
|
||||
.map(({ condition, block }) => `${condition ? `if (${condition}) ` : ''}return ${block};`)
|
||||
.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 = branches[0].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`
|
||||
${hasElse
|
||||
? deindent`
|
||||
${name}.u();
|
||||
${name}.d();
|
||||
`
|
||||
: deindent`
|
||||
if (${name}) {
|
||||
${name}.u();
|
||||
${name}.d();
|
||||
}`}
|
||||
${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.unmount.addLine(`${if_name}${name}.u();`);
|
||||
|
||||
block.builders.destroy.addLine(`${if_name}${name}.d();`);
|
||||
}
|
||||
|
||||
// if any of the siblings have outros, we need to keep references to the blocks
|
||||
// (TODO does this only apply to bidi transitions?)
|
||||
buildCompoundWithOutros(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string,
|
||||
branches,
|
||||
dynamic,
|
||||
{ name, anchor, hasElse }
|
||||
) {
|
||||
const select_block_type = block.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} = [
|
||||
${branches.map(branch => branch.block).join(',\n')}
|
||||
];
|
||||
|
||||
var ${if_blocks} = [];
|
||||
|
||||
function ${select_block_type}(ctx) {
|
||||
${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 = branches[0].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`
|
||||
${name}.o(function() {
|
||||
${if_blocks}[ ${previous_block_index} ].u();
|
||||
${if_blocks}[ ${previous_block_index} ].d();
|
||||
${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}].u();
|
||||
${if_blocks}[${current_block_type_index}].d();
|
||||
}
|
||||
`);
|
||||
}
|
||||
|
||||
buildSimple(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string,
|
||||
branch,
|
||||
dynamic,
|
||||
{ name, anchor, if_name }
|
||||
) {
|
||||
block.builders.init.addBlock(deindent`
|
||||
var ${name} = (${branch.condition}) && ${branch.block}(#component, ctx);
|
||||
`);
|
||||
|
||||
const mountOrIntro = branch.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.hasIntroMethod
|
||||
? deindent`
|
||||
if (${name}) {
|
||||
${name}.p(changed, ctx);
|
||||
} else {
|
||||
${name} = ${branch.block}(#component, ctx);
|
||||
if (${name}) ${name}.c();
|
||||
}
|
||||
|
||||
${name}.i(${updateMountNode}, ${anchor});
|
||||
`
|
||||
: deindent`
|
||||
if (${name}) {
|
||||
${name}.p(changed, ctx);
|
||||
} else {
|
||||
${name} = ${branch.block}(#component, ctx);
|
||||
${name}.c();
|
||||
${name}.m(${updateMountNode}, ${anchor});
|
||||
}
|
||||
`
|
||||
: branch.hasIntroMethod
|
||||
? deindent`
|
||||
if (!${name}) {
|
||||
${name} = ${branch.block}(#component, ctx);
|
||||
${name}.c();
|
||||
}
|
||||
${name}.i(${updateMountNode}, ${anchor});
|
||||
`
|
||||
: deindent`
|
||||
if (!${name}) {
|
||||
${name} = ${branch.block}(#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.hasOutroMethod
|
||||
? deindent`
|
||||
${name}.o(function() {
|
||||
${name}.u();
|
||||
${name}.d();
|
||||
${name} = null;
|
||||
});
|
||||
`
|
||||
: deindent`
|
||||
${name}.u();
|
||||
${name}.d();
|
||||
${name} = null;
|
||||
`;
|
||||
|
||||
block.builders.update.addBlock(deindent`
|
||||
if (${branch.condition}) {
|
||||
${enter}
|
||||
} else if (${name}) {
|
||||
${exit}
|
||||
}
|
||||
`);
|
||||
|
||||
block.builders.unmount.addLine(`${if_name}${name}.u();`);
|
||||
|
||||
block.builders.destroy.addLine(`${if_name}${name}.d();`);
|
||||
}
|
||||
|
||||
getBranches(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string,
|
||||
node: IfBlock
|
||||
) {
|
||||
const branches = [
|
||||
{
|
||||
condition: node.expression.snippet,
|
||||
block: node.block.name,
|
||||
hasUpdateMethod: node.block.hasUpdateMethod,
|
||||
hasIntroMethod: node.block.hasIntroMethod,
|
||||
hasOutroMethod: node.block.hasOutroMethod,
|
||||
},
|
||||
];
|
||||
|
||||
this.visitChildren(block, node);
|
||||
|
||||
if (isElseIf(node.else)) {
|
||||
branches.push(
|
||||
...this.getBranches(block, parentNode, parentNodes, node.else.children[0])
|
||||
);
|
||||
} else {
|
||||
branches.push({
|
||||
condition: null,
|
||||
block: node.else ? node.else.block.name : null,
|
||||
hasUpdateMethod: node.else ? node.else.block.hasUpdateMethod : false,
|
||||
hasIntroMethod: node.else ? node.else.block.hasIntroMethod : false,
|
||||
hasOutroMethod: node.else ? node.else.block.hasOutroMethod : false,
|
||||
});
|
||||
|
||||
if (node.else) {
|
||||
this.visitChildren(block, node.else);
|
||||
}
|
||||
}
|
||||
|
||||
return branches;
|
||||
}
|
||||
|
||||
ssr() {
|
||||
const { compiler } = this;
|
||||
const { snippet } = this.expression;
|
||||
|
||||
compiler.target.append('${ ' + snippet + ' ? `');
|
||||
|
||||
this.children.forEach((child: Node) => {
|
||||
child.ssr();
|
||||
});
|
||||
|
||||
compiler.target.append('` : `');
|
||||
|
||||
if (this.else) {
|
||||
this.else.children.forEach((child: Node) => {
|
||||
child.ssr();
|
||||
});
|
||||
}
|
||||
|
||||
compiler.target.append('` }');
|
||||
}
|
||||
|
||||
visitChildren(block: Block, node: Node) {
|
||||
node.children.forEach((child: Node) => {
|
||||
child.build(node.block, null, 'nodes');
|
||||
});
|
||||
}
|
||||
}
|
@ -1,172 +0,0 @@
|
||||
import Compiler from './../../Compiler';
|
||||
import Block from '../../dom/Block';
|
||||
import { trimStart, trimEnd } from '../../../utils/trim';
|
||||
|
||||
export default class Node {
|
||||
readonly start: number;
|
||||
readonly end: number;
|
||||
readonly compiler: Compiler;
|
||||
readonly parent: Node;
|
||||
readonly type: string;
|
||||
|
||||
prev?: Node;
|
||||
next?: Node;
|
||||
|
||||
canUseInnerHTML: boolean;
|
||||
var: string;
|
||||
|
||||
constructor(compiler: Compiler, parent, scope, info: any) {
|
||||
this.start = info.start;
|
||||
this.end = info.end;
|
||||
this.type = info.type;
|
||||
|
||||
// this makes properties non-enumerable, which makes logging
|
||||
// bearable. might have a performance cost. TODO remove in prod?
|
||||
Object.defineProperties(this, {
|
||||
compiler: {
|
||||
value: compiler
|
||||
},
|
||||
parent: {
|
||||
value: parent
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cannotUseInnerHTML() {
|
||||
if (this.canUseInnerHTML !== false) {
|
||||
this.canUseInnerHTML = false;
|
||||
if (this.parent) this.parent.cannotUseInnerHTML();
|
||||
}
|
||||
}
|
||||
|
||||
init(
|
||||
block: Block,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Node
|
||||
) {
|
||||
// implemented by subclasses
|
||||
}
|
||||
|
||||
initChildren(
|
||||
block: Block,
|
||||
stripWhitespace: boolean,
|
||||
nextSibling: Node
|
||||
) {
|
||||
// glue text nodes together
|
||||
const cleaned: Node[] = [];
|
||||
let lastChild: Node;
|
||||
|
||||
let windowComponent;
|
||||
|
||||
this.children.forEach((child: Node) => {
|
||||
if (child.type === 'Comment') return;
|
||||
|
||||
// special case — this is an easy way to remove whitespace surrounding
|
||||
// <svelte:window/>. lil hacky but it works
|
||||
if (child.type === 'Window') {
|
||||
windowComponent = child;
|
||||
return;
|
||||
}
|
||||
|
||||
if (child.type === 'Text' && lastChild && lastChild.type === 'Text') {
|
||||
lastChild.data += child.data;
|
||||
lastChild.end = child.end;
|
||||
} else {
|
||||
if (child.type === 'Text' && stripWhitespace && cleaned.length === 0) {
|
||||
child.data = trimStart(child.data);
|
||||
if (child.data) cleaned.push(child);
|
||||
} else {
|
||||
cleaned.push(child);
|
||||
}
|
||||
}
|
||||
|
||||
lastChild = child;
|
||||
});
|
||||
|
||||
lastChild = null;
|
||||
|
||||
cleaned.forEach((child: Node, i: number) => {
|
||||
child.canUseInnerHTML = !this.compiler.options.hydratable;
|
||||
|
||||
child.init(block, stripWhitespace, cleaned[i + 1] || nextSibling);
|
||||
|
||||
if (child.shouldSkip) return;
|
||||
|
||||
if (lastChild) lastChild.next = child;
|
||||
child.prev = lastChild;
|
||||
|
||||
lastChild = child;
|
||||
});
|
||||
|
||||
// We want to remove trailing whitespace inside an element/component/block,
|
||||
// *unless* there is no whitespace between this node and its next sibling
|
||||
if (stripWhitespace && lastChild && lastChild.type === 'Text') {
|
||||
const shouldTrim = (
|
||||
nextSibling ? (nextSibling.type === 'Text' && /^\s/.test(nextSibling.data)) : !this.hasAncestor('EachBlock')
|
||||
);
|
||||
|
||||
if (shouldTrim) {
|
||||
lastChild.data = trimEnd(lastChild.data);
|
||||
if (!lastChild.data) {
|
||||
cleaned.pop();
|
||||
lastChild = cleaned[cleaned.length - 1];
|
||||
lastChild.next = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.children = cleaned;
|
||||
if (windowComponent) cleaned.unshift(windowComponent);
|
||||
}
|
||||
|
||||
build(
|
||||
block: Block,
|
||||
parentNode: string,
|
||||
parentNodes: string
|
||||
) {
|
||||
// implemented by subclasses
|
||||
}
|
||||
|
||||
isDomNode() {
|
||||
return this.type === 'Element' || this.type === 'Text' || this.type === 'MustacheTag';
|
||||
}
|
||||
|
||||
hasAncestor(type: string) {
|
||||
return this.parent ?
|
||||
this.parent.type === type || this.parent.hasAncestor(type) :
|
||||
false;
|
||||
}
|
||||
|
||||
findNearest(selector: RegExp) {
|
||||
if (selector.test(this.type)) return this;
|
||||
if (this.parent) return this.parent.findNearest(selector);
|
||||
}
|
||||
|
||||
getOrCreateAnchor(block: Block, parentNode: string, parentNodes: string) {
|
||||
// TODO use this in EachBlock and IfBlock — tricky because
|
||||
// children need to be created first
|
||||
const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode();
|
||||
const anchor = needsAnchor
|
||||
? block.getUniqueName(`${this.var}_anchor`)
|
||||
: (this.next && this.next.var) || 'null';
|
||||
|
||||
if (needsAnchor) {
|
||||
block.addElement(
|
||||
anchor,
|
||||
`@createComment()`,
|
||||
parentNodes && `@createComment()`,
|
||||
parentNode
|
||||
);
|
||||
}
|
||||
|
||||
return anchor;
|
||||
}
|
||||
|
||||
getUpdateMountNode(anchor: string) {
|
||||
return this.parent.isDomNode() ? this.parent.var : `${anchor}.parentNode`;
|
||||
}
|
||||
|
||||
remount(name: string) {
|
||||
return `${this.var}.m(${name}._slotted.default, null);`;
|
||||
}
|
||||
}
|
@ -1,175 +0,0 @@
|
||||
import deindent from '../../utils/deindent';
|
||||
import Compiler from '../Compiler';
|
||||
import Stats from '../../Stats';
|
||||
import Stylesheet from '../../css/Stylesheet';
|
||||
import { removeNode, removeObjectKey } from '../../utils/removeNode';
|
||||
import getName from '../../utils/getName';
|
||||
import globalWhitelist from '../../utils/globalWhitelist';
|
||||
import { Ast, Node, CompileOptions } from '../../interfaces';
|
||||
import { AppendTarget } from '../../interfaces';
|
||||
import { stringify } from '../../utils/stringify';
|
||||
|
||||
export class SsrTarget {
|
||||
bindings: string[];
|
||||
renderCode: string;
|
||||
appendTargets: AppendTarget[];
|
||||
|
||||
constructor() {
|
||||
this.bindings = [];
|
||||
this.renderCode = '';
|
||||
this.appendTargets = [];
|
||||
}
|
||||
|
||||
append(code: string) {
|
||||
if (this.appendTargets.length) {
|
||||
const appendTarget = this.appendTargets[this.appendTargets.length - 1];
|
||||
const slotName = appendTarget.slotStack[appendTarget.slotStack.length - 1];
|
||||
appendTarget.slots[slotName] += code;
|
||||
} else {
|
||||
this.renderCode += code;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function ssr(
|
||||
ast: Ast,
|
||||
source: string,
|
||||
stylesheet: Stylesheet,
|
||||
options: CompileOptions,
|
||||
stats: Stats
|
||||
) {
|
||||
const format = options.format || 'cjs';
|
||||
|
||||
const target = new SsrTarget();
|
||||
const compiler = new Compiler(ast, source, options.name || 'SvelteComponent', stylesheet, options, stats, false, target);
|
||||
|
||||
const { computations, name, templateProperties } = compiler;
|
||||
|
||||
// create main render() function
|
||||
trim(compiler.fragment.children).forEach((node: Node) => {
|
||||
node.ssr();
|
||||
});
|
||||
|
||||
const css = compiler.customElement ?
|
||||
{ code: null, map: null } :
|
||||
compiler.stylesheet.render(options.filename, true);
|
||||
|
||||
// generate initial state object
|
||||
const expectedProperties = Array.from(compiler.expectedProperties);
|
||||
const globals = expectedProperties.filter(prop => globalWhitelist.has(prop));
|
||||
const storeProps = expectedProperties.filter(prop => prop[0] === '$');
|
||||
|
||||
const initialState = [];
|
||||
if (globals.length > 0) {
|
||||
initialState.push(`{ ${globals.map(prop => `${prop} : ${prop}`).join(', ')} }`);
|
||||
}
|
||||
|
||||
if (storeProps.length > 0) {
|
||||
const initialize = `_init([${storeProps.map(prop => `"${prop.slice(1)}"`)}])`
|
||||
initialState.push(`options.store.${initialize}`);
|
||||
}
|
||||
|
||||
if (templateProperties.data) {
|
||||
initialState.push(`%data()`);
|
||||
} else if (globals.length === 0 && storeProps.length === 0) {
|
||||
initialState.push('{}');
|
||||
}
|
||||
|
||||
initialState.push('ctx');
|
||||
|
||||
const helpers = new Set();
|
||||
|
||||
// TODO concatenate CSS maps
|
||||
const result = deindent`
|
||||
${compiler.javascript}
|
||||
|
||||
var ${name} = {};
|
||||
|
||||
${options.filename && `${name}.filename = ${stringify(options.filename)}`};
|
||||
|
||||
${name}.data = function() {
|
||||
return ${templateProperties.data ? `%data()` : `{}`};
|
||||
};
|
||||
|
||||
${name}.render = function(state, options = {}) {
|
||||
var components = new Set();
|
||||
|
||||
function addComponent(component) {
|
||||
components.add(component);
|
||||
}
|
||||
|
||||
var result = { head: '', addComponent };
|
||||
var html = ${name}._render(result, state, options);
|
||||
|
||||
var cssCode = Array.from(components).map(c => c.css && c.css.code).filter(Boolean).join('\\n');
|
||||
|
||||
return {
|
||||
html,
|
||||
head: result.head,
|
||||
css: { code: cssCode, map: null },
|
||||
toString() {
|
||||
return html;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
${name}._render = function(__result, ctx, options) {
|
||||
${templateProperties.store && `options.store = %store();`}
|
||||
__result.addComponent(${name});
|
||||
|
||||
ctx = Object.assign(${initialState.join(', ')});
|
||||
|
||||
${computations.map(
|
||||
({ key, deps }) =>
|
||||
`ctx.${key} = %computed-${key}(ctx);`
|
||||
)}
|
||||
|
||||
${target.bindings.length &&
|
||||
deindent`
|
||||
var settled = false;
|
||||
var tmp;
|
||||
|
||||
while (!settled) {
|
||||
settled = true;
|
||||
|
||||
${target.bindings.join('\n\n')}
|
||||
}
|
||||
`}
|
||||
|
||||
return \`${target.renderCode}\`;
|
||||
};
|
||||
|
||||
${name}.css = {
|
||||
code: ${css.code ? stringify(css.code) : `''`},
|
||||
map: ${css.map ? stringify(css.map.toString()) : 'null'}
|
||||
};
|
||||
|
||||
var warned = false;
|
||||
|
||||
${templateProperties.preload && `${name}.preload = %preload;`}
|
||||
`;
|
||||
|
||||
return compiler.generate(result, options, { name, format });
|
||||
}
|
||||
|
||||
function trim(nodes) {
|
||||
let start = 0;
|
||||
for (; start < nodes.length; start += 1) {
|
||||
const node = nodes[start];
|
||||
if (node.type !== 'Text') break;
|
||||
|
||||
node.data = node.data.replace(/^\s+/, '');
|
||||
if (node.data) break;
|
||||
}
|
||||
|
||||
let end = nodes.length;
|
||||
for (; end > start; end -= 1) {
|
||||
const node = nodes[end - 1];
|
||||
if (node.type !== 'Text') break;
|
||||
|
||||
node.data = node.data.replace(/\s+$/, '');
|
||||
if (node.data) break;
|
||||
}
|
||||
|
||||
return nodes.slice(start, end);
|
||||
}
|
Loading…
Reference in new issue