Merge pull request #649 from sveltejs/hydration

[WIP] Hydration
pull/7738/head
Rich Harris 8 years ago committed by GitHub
commit f2e30c06c5

@ -40,7 +40,10 @@ export default class Block {
listName: string;
builders: {
init: CodeBuilder;
create: CodeBuilder;
claim: CodeBuilder;
hydrate: CodeBuilder;
mount: CodeBuilder;
intro: CodeBuilder;
update: CodeBuilder;
@ -86,7 +89,10 @@ export default class Block {
this.listName = options.listName;
this.builders = {
init: new CodeBuilder(),
create: new CodeBuilder(),
claim: new CodeBuilder(),
hydrate: new CodeBuilder(),
mount: new CodeBuilder(),
intro: new CodeBuilder(),
update: new CodeBuilder(),
@ -111,7 +117,7 @@ export default class Block {
this.hasUpdateMethod = false; // determined later
}
addDependencies(dependencies) {
addDependencies(dependencies: string[]) {
dependencies.forEach(dependency => {
this.dependencies.add(dependency);
});
@ -120,21 +126,17 @@ export default class Block {
addElement(
name: string,
renderStatement: string,
claimStatement: string,
parentNode: string,
needsIdentifier = false
) {
const isToplevel = !parentNode;
if (needsIdentifier || isToplevel) {
this.builders.create.addLine(`var ${name} = ${renderStatement};`);
this.mount(name, parentNode);
} else {
this.builders.create.addLine(
`${this.generator.helper(
'appendNode'
)}( ${renderStatement}, ${parentNode} );`
);
}
this.addVariable(name);
this.builders.create.addLine(`${name} = ${renderStatement};`);
this.builders.claim.addLine(`${name} = ${claimStatement};`)
this.mount(name, parentNode);
if (isToplevel) {
this.builders.unmount.addLine(
@ -184,7 +186,7 @@ export default class Block {
mount(name: string, parentNode: string) {
if (parentNode) {
this.builders.create.addLine(
this.builders.mount.addLine(
`${this.generator.helper('appendNode')}( ${name}, ${parentNode} );`
);
} else {
@ -210,19 +212,8 @@ export default class Block {
this.addVariable(outroing);
}
if (this.variables.size) {
const variables = Array.from(this.variables.keys())
.map(key => {
const init = this.variables.get(key);
return init !== undefined ? `${key} = ${init}` : key;
})
.join(', ');
this.builders.create.addBlockAtStart(`var ${variables};`);
}
if (this.autofocus) {
this.builders.create.addLine(`${this.autofocus}.focus();`);
this.builders.mount.addLine(`${this.autofocus}.focus();`);
}
// minor hack we need to ensure that any {{{triples}}} are detached first
@ -237,7 +228,40 @@ export default class Block {
}
if (this.first) {
properties.addBlock(`first: ${this.first},`);
properties.addBlock(`first: null,`);
this.builders.hydrate.addLine( `this.first = ${this.first};` );
}
if (this.builders.create.isEmpty()) {
properties.addBlock(`create: ${this.generator.helper('noop')},`);
} else {
properties.addBlock(deindent`
create: function () {
${this.builders.create}
${!this.builders.hydrate.isEmpty() && `this.hydrate();`}
},
`);
}
if (this.generator.hydratable) {
if (this.builders.claim.isEmpty()) {
properties.addBlock(`claim: ${this.generator.helper('noop')},`);
} else {
properties.addBlock(deindent`
claim: function ( nodes ) {
${this.builders.claim}
${!this.builders.hydrate.isEmpty() && `this.hydrate();`}
},
`);
}
}
if (!this.builders.hydrate.isEmpty()) {
properties.addBlock(deindent`
hydrate: function ( nodes ) {
${this.builders.hydrate}
},
`);
}
if (this.builders.mount.isEmpty()) {
@ -331,7 +355,13 @@ export default class Block {
.key
? `, ${localKey}`
: ''} ) {
${this.builders.create}
${this.variables.size > 0 && (
`var ${Array.from(this.variables.keys()).map(key => {
const init = this.variables.get(key);
return init !== undefined ? `${key} = ${init}` : key;
}).join(', ')};`)}
${!this.builders.init.isEmpty() && this.builders.init}
return {
${properties}

@ -18,6 +18,8 @@ export class DomGenerator extends Generator {
readonly: Set<string>;
metaBindings: string[];
hydratable: boolean;
hasIntroTransitions: boolean;
hasOutroTransitions: boolean;
hasComplexBindings: boolean;
@ -34,6 +36,8 @@ export class DomGenerator extends Generator {
this.readonly = new Set();
this.hydratable = options.hydratable;
// initial values for e.g. window.innerWidth, if there's a <:Window> meta tag
this.metaBindings = [];
}
@ -234,7 +238,20 @@ export default function dom(
this._fragment = ${generator.alias(
'create_main_fragment'
)}( this._state, this );
if ( options.target ) this._fragment.mount( options.target, null );
if ( options.target ) {
${generator.hydratable ?
deindent`
var nodes = ${generator.helper('children')}( options.target );
options.hydrate ? this._fragment.claim( nodes ) : this._fragment.create();
nodes.forEach( ${generator.helper('detachNode')} );
` :
deindent`
this._fragment.create();
`}
this._fragment.mount( options.target, null );
}
${generator.hasComplexBindings &&
`while ( this._bindings.length ) this._bindings.pop()();`}
${(generator.hasComponents || generator.hasIntroTransitions) &&

@ -2,6 +2,7 @@ export interface State {
name: string;
namespace: string;
parentNode: string;
parentNodes: string;
isTopLevel: boolean;
parentNodeName?: string;
basename?: string;

@ -12,7 +12,7 @@ function isElseIf(node: Node) {
}
function getChildState(parent: State, child = {}) {
return assign({}, parent, { name: null, parentNode: null }, child || {});
return assign({}, parent, { name: null, parentNode: null, parentNodes: 'nodes' }, child || {});
}
// Whitespace inside one of these elements will not result in
@ -285,6 +285,7 @@ const preprocessors = {
isTopLevel: false,
name,
parentNode: name,
parentNodes: block.getUniqueName(`${name}_nodes`),
parentNodeName: node.name,
namespace: node.name === 'svg'
? 'http://www.w3.org/2000/svg'
@ -398,6 +399,7 @@ export default function preprocess(
const state: State = {
namespace,
parentNode: null,
parentNodes: 'nodes',
isTopLevel: true,
};

@ -113,7 +113,6 @@ export default function visitComponent(
}
const componentInitProperties = [
`target: ${!isTopLevel ? state.parentNode : 'null'}`,
`_root: ${block.component}._root`,
];
@ -123,16 +122,24 @@ export default function visitComponent(
const childBlock = node._block;
node.children.forEach(child => {
node.children.forEach((child: Node) => {
visit(generator, childBlock, childState, child);
});
const yieldFragment = block.getUniqueName(`${name}_yield_fragment`);
block.builders.create.addLine(
block.builders.init.addLine(
`var ${yieldFragment} = ${childBlock.name}( ${params}, ${block.component} );`
);
block.builders.create.addLine(
`${yieldFragment}.create();`
);
block.builders.claim.addLine(
`${yieldFragment}.claim( ${state.parentNodes} );`
);
if (childBlock.hasUpdateMethod) {
block.builders.update.addLine(
`${yieldFragment}.update( changed, ${params} );`
@ -186,12 +193,6 @@ export default function visitComponent(
});
`);
if (isTopLevel) {
block.builders.mount.addLine(
`${name}._fragment.mount( ${block.target}, anchor );`
);
}
if (local.dynamicAttributes.length) {
const updates = local.dynamicAttributes.map(attribute => {
if (attribute.dependencies.length) {
@ -222,6 +223,14 @@ export default function visitComponent(
block.builders.unmount.addLine(`${name}._fragment.unmount();`);
block.builders.destroy.addLine(`${name}.destroy( false );`);
block.builders.create.addBlock(local.create);
block.builders.init.addBlock(local.create);
const targetNode = state.parentNode || block.target;
const anchorNode = state.parentNode ? 'null' : 'anchor';
block.builders.create.addLine(`${name}._fragment.create();`);
block.builders.claim.addLine(`${name}._fragment.claim( ${state.parentNodes} );`);
block.builders.mount.addLine(`${name}._fragment.mount( ${targetNode}, ${anchorNode} );` );
if (!local.update.isEmpty()) block.builders.update.addBlock(local.update);
}

@ -35,7 +35,7 @@ export default function visitEachBlock(
const { snippet } = block.contextualise(node.expression);
block.builders.create.addLine(`var ${each_block_value} = ${snippet};`);
block.builders.init.addLine(`var ${each_block_value} = ${snippet};`);
if (node.key) {
keyed(generator, block, state, node, snippet, vars);
@ -49,6 +49,7 @@ export default function visitEachBlock(
block.addElement(
anchor,
`${generator.helper('createComment')}()`,
`${generator.helper('createComment')}()`,
state.parentNode,
true
);
@ -59,23 +60,18 @@ export default function visitEachBlock(
if (node.else) {
const each_block_else = generator.getUniqueName(`${each_block}_else`);
block.builders.create.addLine(`var ${each_block_else} = null;`);
block.builders.init.addLine(`var ${each_block_else} = null;`);
// TODO neaten this up... will end up with an empty line in the block
block.builders.create.addBlock(deindent`
block.builders.init.addBlock(deindent`
if ( !${each_block_value}.length ) {
${each_block_else} = ${node.else._block
.name}( ${params}, ${block.component} );
${!isToplevel
? `${each_block_else}.${mountOrIntro}( ${state.parentNode}, null );`
: ''}
${each_block_else} = ${node.else._block.name}( ${params}, ${block.component} );
}
`);
block.builders.mount.addBlock(deindent`
if ( ${each_block_else} ) {
${each_block_else}.${mountOrIntro}( ${state.parentNode ||
block.target}, null );
${each_block_else}.${mountOrIntro}( ${state.parentNode || block.target}, null );
}
`);
@ -86,8 +82,8 @@ export default function visitEachBlock(
if ( !${each_block_value}.length && ${each_block_else} ) {
${each_block_else}.update( changed, ${params} );
} else if ( !${each_block_value}.length ) {
${each_block_else} = ${node.else._block
.name}( ${params}, ${block.component} );
${each_block_else} = ${node.else._block.name}( ${params}, ${block.component} );
${each_block_else}.create();
${each_block_else}.${mountOrIntro}( ${parentNode}, ${anchor} );
} else if ( ${each_block_else} ) {
${each_block_else}.unmount();
@ -104,8 +100,8 @@ export default function visitEachBlock(
${each_block_else} = null;
}
} else if ( !${each_block_else} ) {
${each_block_else} = ${node.else._block
.name}( ${params}, ${block.component} );
${each_block_else} = ${node.else._block.name}( ${params}, ${block.component} );
${each_block_else}.create();
${each_block_else}.${mountOrIntro}( ${parentNode}, ${anchor} );
}
`);
@ -136,7 +132,7 @@ function keyed(
block: Block,
state: State,
node: Node,
snippet,
snippet: string,
{
each_block,
create_each_block,
@ -154,6 +150,10 @@ function keyed(
const last = block.getUniqueName(`${each_block}_last`);
const expected = block.getUniqueName(`${each_block}_expected`);
block.addVariable(lookup, `Object.create( null )`);
block.addVariable(head);
block.addVariable(last);
if (node.children[0] && node.children[0].type === 'Element') {
// TODO or text/tag/raw
node._block.first = node.children[0]._state.parentNode; // TODO this is highly confusing
@ -162,22 +162,16 @@ function keyed(
node._block.addElement(
node._block.first,
`${generator.helper('createComment')}()`,
`${generator.helper('createComment')}()`,
null,
true
);
}
block.builders.create.addBlock(deindent`
var ${lookup} = Object.create( null );
var ${head};
var ${last};
block.builders.init.addBlock(deindent`
for ( var ${i} = 0; ${i} < ${each_block_value}.length; ${i} += 1 ) {
var ${key} = ${each_block_value}[${i}].${node.key};
var ${iteration} = ${lookup}[${key}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component}, ${key} );
${state.parentNode &&
`${iteration}.${mountOrIntro}( ${state.parentNode}, null );`}
if ( ${last} ) ${last}.next = ${iteration};
${iteration}.last = ${last};
@ -187,15 +181,32 @@ function keyed(
}
`);
if (!state.parentNode) {
block.builders.mount.addBlock(deindent`
var ${iteration} = ${head};
while ( ${iteration} ) {
${iteration}.${mountOrIntro}( ${block.target}, anchor );
${iteration} = ${iteration}.next;
}
`);
}
const targetNode = state.parentNode || block.target;
const anchorNode = state.parentNode ? 'null' : 'anchor';
block.builders.create.addBlock(deindent`
var ${iteration} = ${head};
while ( ${iteration} ) {
${iteration}.create();
${iteration} = ${iteration}.next;
}
`);
block.builders.claim.addBlock(deindent`
var ${iteration} = ${head};
while ( ${iteration} ) {
${iteration}.claim( ${state.parentNodes} );
${iteration} = ${iteration}.next;
}
`);
block.builders.mount.addBlock(deindent`
var ${iteration} = ${head};
while ( ${iteration} ) {
${iteration}.${mountOrIntro}( ${targetNode}, ${anchorNode} );
${iteration} = ${iteration}.next;
}
`);
const dynamic = node._block.hasUpdateMethod;
const parentNode = state.parentNode || `${anchor}.parentNode`;
@ -203,7 +214,7 @@ function keyed(
let destroy;
if (node._block.hasOutroMethod) {
const fn = block.getUniqueName(`${each_block}_outro`);
block.builders.create.addBlock(deindent`
block.builders.init.addBlock(deindent`
function ${fn} ( iteration ) {
iteration.outro( function () {
iteration.unmount();
@ -227,7 +238,7 @@ function keyed(
`;
} else {
const fn = block.getUniqueName(`${each_block}_destroy`);
block.builders.create.addBlock(deindent`
block.builders.init.addBlock(deindent`
function ${fn} ( iteration ) {
iteration.unmount();
iteration.destroy();
@ -271,7 +282,6 @@ function keyed(
} else {
if ( ${iteration} ) {
// probably a deletion
while ( ${expected} && ${expected}.key !== ${key} ) {
${expected}.discard = true;
discard_pile.push( ${expected} );
@ -286,6 +296,7 @@ function keyed(
} else {
// key is being inserted
${iteration} = ${lookup}[${key}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component}, ${key} );
${iteration}.create();
${iteration}.${mountOrIntro}( ${parentNode}, ${expected}.first );
${expected}.last = ${iteration};
@ -300,6 +311,7 @@ function keyed(
${iteration}.mount( ${parentNode}, ${anchor} );
} else {
${iteration} = ${lookup}[${key}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component}, ${key} );
${iteration}.create();
${iteration}.${mountOrIntro}( ${parentNode}, ${anchor} );
}
}
@ -353,27 +365,38 @@ function unkeyed(
mountOrIntro,
}
) {
block.builders.create.addBlock(deindent`
block.builders.init.addBlock(deindent`
var ${iterations} = [];
for ( var ${i} = 0; ${i} < ${each_block_value}.length; ${i} += 1 ) {
${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} );
${state.parentNode &&
`${iterations}[${i}].${mountOrIntro}( ${state.parentNode}, null );`}
}
`);
if (!state.parentNode) {
block.builders.mount.addBlock(deindent`
for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) {
${iterations}[${i}].${mountOrIntro}( ${block.target}, anchor );
}
`);
}
const targetNode = state.parentNode || block.target;
const anchorNode = state.parentNode ? 'null' : 'anchor';
block.builders.create.addBlock(deindent`
for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) {
${iterations}[${i}].create();
}
`);
block.builders.claim.addBlock(deindent`
for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) {
${iterations}[${i}].claim( ${state.parentNodes} );
}
`);
block.builders.mount.addBlock(deindent`
for ( var ${i} = 0; ${i} < ${iterations}.length; ${i} += 1 ) {
${iterations}[${i}].${mountOrIntro}( ${targetNode}, ${anchorNode} );
}
`);
const dependencies = block.findDependencies(node.expression);
const allDependencies = new Set(node._block.dependencies);
dependencies.forEach(dependency => {
dependencies.forEach((dependency: string) => {
allDependencies.add(dependency);
});
@ -392,6 +415,7 @@ function unkeyed(
${iterations}[${i}].update( changed, ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i} );
} else {
${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} );
${iterations}[${i}].create();
}
${iterations}[${i}].intro( ${parentNode}, ${anchor} );
`
@ -400,6 +424,7 @@ function unkeyed(
${iterations}[${i}].update( changed, ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i} );
} else {
${iterations}[${i}] = ${create_each_block}( ${params}, ${each_block_value}, ${each_block_value}[${i}], ${i}, ${block.component} );
${iterations}[${i}].create();
${iterations}[${i}].mount( ${parentNode}, ${anchor} );
}
`

@ -22,7 +22,11 @@ export default function visitAttribute(
const isIndirectlyBoundValue =
name === 'value' &&
(node.name === 'option' || // TODO check it's actually bound
(node.name === 'input' && node.attributes.find((attribute: Node) => attribute.type === 'Binding' && /checked|group/.test(attribute.name))));
(node.name === 'input' &&
node.attributes.find(
(attribute: Node) =>
attribute.type === 'Binding' && /checked|group/.test(attribute.name)
)));
const propertyName = isIndirectlyBoundValue
? '__value'
@ -76,7 +80,7 @@ export default function visitAttribute(
// annoying special case
const isMultipleSelect =
node.name === 'select' &&
node.attributes.find(attr => attr.name.toLowerCase() === 'multiple'); // TODO use getStaticAttributeValue
node.attributes.find((attr: Node) => attr.name.toLowerCase() === 'multiple'); // TODO use getStaticAttributeValue
const i = block.getUniqueName('i');
const option = block.getUniqueName('option');
@ -97,17 +101,17 @@ export default function visitAttribute(
}
`;
block.builders.create.addLine(deindent`
block.builders.hydrate.addLine(deindent`
${last} = ${value}
${updater}
`);
} else if (propertyName) {
block.builders.create.addLine(
block.builders.hydrate.addLine(
`${state.parentNode}.${propertyName} = ${last} = ${value};`
);
updater = `${state.parentNode}.${propertyName} = ${last};`;
} else {
block.builders.create.addLine(
block.builders.hydrate.addLine(
`${generator.helper(
method
)}( ${state.parentNode}, '${name}', ${last} = ${value} );`
@ -132,10 +136,10 @@ export default function visitAttribute(
const statement = propertyName
? `${state.parentNode}.${propertyName} = ${value};`
: `${generator.helper(
method
)}( ${state.parentNode}, '${name}', ${value} );`;
method
)}( ${state.parentNode}, '${name}', ${value} );`;
block.builders.create.addLine(statement);
block.builders.hydrate.addLine(statement);
// special case autofocus. has to be handled in a bit of a weird way
if (attribute.value === true && name === 'autofocus') {
@ -152,7 +156,7 @@ export default function visitAttribute(
if (isIndirectlyBoundValue) {
const updateValue = `${state.parentNode}.value = ${state.parentNode}.__value;`;
block.builders.create.addLine(updateValue);
block.builders.hydrate.addLine(updateValue);
if (isDynamic) block.builders.update.addLine(updateValue);
}
}

@ -91,7 +91,7 @@ export default function visitBinding(
`;
generator.hasComplexBindings = true;
block.builders.create.addBlock(
block.builders.hydrate.addBlock(
`if ( !('${name}' in state) ) ${block.component}._bindings.push( ${handler} );`
);
} else if (attribute.name === 'group') {
@ -107,7 +107,7 @@ export default function visitBinding(
? `~${snippet}.indexOf( ${state.parentNode}.__value )`
: `${state.parentNode}.__value === ${snippet}`;
block.builders.create.addLine(
block.builders.hydrate.addLine(
`${block.component}._bindingGroups[${bindingGroup}].push( ${state.parentNode} );`
);
@ -118,7 +118,7 @@ export default function visitBinding(
updateElement = `${state.parentNode}.checked = ${condition};`;
} else if (node.name === 'audio' || node.name === 'video') {
generator.hasComplexBindings = true;
block.builders.create.addBlock(
block.builders.hydrate.addBlock(
`${block.component}._bindings.push( ${handler} );`
);
@ -144,13 +144,15 @@ export default function visitBinding(
}
}
block.builders.create.addBlock(deindent`
block.builders.init.addBlock(deindent`
function ${handler} () {
${lock} = true;
${setter}
${lock} = false;
}
`);
block.builders.hydrate.addBlock(deindent`
${generator.helper(
'addListener'
)}( ${state.parentNode}, '${eventName}', ${handler} );

@ -6,6 +6,7 @@ import visitAttribute from './Attribute';
import visitEventHandler from './EventHandler';
import visitBinding from './Binding';
import visitRef from './Ref';
import * as namespaces from '../../../../utils/namespaces';
import addTransitions from './addTransitions';
import { DomGenerator } from '../../index';
import Block from '../../Block';
@ -47,18 +48,27 @@ export default function visitElement(
const childState = node._state;
const name = childState.parentNode;
block.builders.create.addLine(
`var ${name} = ${getRenderStatement(
generator,
childState.namespace,
node.name
)};`
);
block.mount(name, state.parentNode);
const isToplevel = !state.parentNode;
block.addVariable(name);
block.builders.create.addLine(`${name} = ${getRenderStatement(generator, childState.namespace, node.name)};`);
if (generator.hydratable) {
block.builders.claim.addBlock(deindent`
${name} = ${getClaimStatement(generator, childState.namespace, state.parentNodes, node)};
var ${childState.parentNodes} = ${generator.helper('children')}( ${name} );
`);
}
if (state.parentNode) {
block.builders.mount.addLine(`${block.generator.helper('appendNode')}( ${name}, ${state.parentNode} );`);
} else {
block.builders.mount.addLine(`${block.generator.helper('insertNode')}( ${name}, ${block.target}, anchor );`);
}
// add CSS encapsulation attribute
if (generator.cssId && (!generator.cascade || state.isTopLevel)) {
block.builders.create.addLine(
block.builders.hydrate.addLine(
`${generator.helper(
'setAttribute'
)}( ${name}, '${generator.cssId}', '' );`
@ -107,7 +117,7 @@ export default function visitElement(
});
if (initialProps.length) {
block.builders.create.addBlock(deindent`
block.builders.hydrate.addBlock(deindent`
${name}._svelte = {
${initialProps.join(',\n')}
};
@ -120,7 +130,7 @@ export default function visitElement(
}
}
if (!state.parentNode) {
if (isToplevel) {
// TODO we eventually need to consider what happens to elements
// that belong to the same outgroup as an outroing element...
block.builders.unmount.addLine(
@ -174,8 +184,12 @@ export default function visitElement(
}
if (node.initialUpdate) {
block.builders.create.addBlock(node.initialUpdate);
block.builders.mount.addBlock(node.initialUpdate);
}
block.builders.claim.addLine(
`${childState.parentNodes}.forEach( ${generator.helper('detachNode')} );`
);
}
function getRenderStatement(
@ -193,3 +207,24 @@ function getRenderStatement(
return `${generator.helper('createElement')}( '${name}' )`;
}
function getClaimStatement(
generator: DomGenerator,
namespace: string,
nodes: string,
node: Node
) {
const attributes = node.attributes
.filter((attr: Node) => attr.type === 'Attribute')
.map((attr: Node) => `${quoteProp(attr.name)}: true`)
.join(', ');
const name = namespace ? node.name : node.name.toUpperCase();
return `${generator.helper('claimElement')}( ${nodes}, '${name}', ${attributes ? `{ ${attributes} }` : `{}`}, ${namespace === namespaces.svg ? true : false} )`;
}
function quoteProp(name: string) {
if (/[^a-zA-Z_$0-9]/.test(name)) return `'${name}'`;
return name;
}

@ -68,36 +68,38 @@ export default function visitEventHandler(
[${attribute.expression.start}-${attribute.expression.end}];
`;
const handler = isCustomEvent
? deindent`
var ${handlerName} = ${generator.alias(
if (isCustomEvent) {
block.addVariable(handlerName);
block.builders.hydrate.addBlock(deindent`
${handlerName} = ${generator.alias(
'template'
)}.events.${name}.call( ${block.component}, ${state.parentNode}, function ( event ) {
${handlerBody}
});
`
: deindent`
`);
block.builders.destroy.addLine(deindent`
${handlerName}.teardown();
`);
} else {
const handler = deindent`
function ${handlerName} ( event ) {
${handlerBody}
}
`;
if (shouldHoist) {
generator.blocks.push(
<Block>{
render: () => handler,
}
);
} else {
block.builders.create.addBlock(handler);
}
if (shouldHoist) {
generator.blocks.push(
<Block>{
render: () => handler,
}
);
} else {
block.builders.init.addBlock(handler);
}
if (isCustomEvent) {
block.builders.destroy.addLine(deindent`
${handlerName}.teardown();
`);
} else {
block.builders.create.addLine(deindent`
block.builders.hydrate.addLine(deindent`
${generator.helper(
'addListener'
)}( ${state.parentNode}, '${name}', ${handlerName} );

@ -13,11 +13,11 @@ export default function visitRef(
) {
const name = attribute.name;
block.builders.create.addLine(
block.builders.mount.addLine(
`${block.component}.refs.${name} = ${state.parentNode};`
);
block.builders.destroy.addLine(deindent`
block.builders.unmount.addLine(deindent`
if ( ${block.component}.refs.${name} === ${state.parentNode} ) ${block.component}.refs.${name} = null;
`);

@ -57,7 +57,7 @@ export default function visitWindow(
[${attribute.expression.start}-${attribute.expression.end}];
`;
block.builders.create.addBlock(deindent`
block.builders.init.addBlock(deindent`
function ${handlerName} ( event ) {
${handlerBody}
};
@ -121,7 +121,7 @@ export default function visitWindow(
${event === 'scroll' && `${lock} = false;`}
`;
block.builders.create.addBlock(deindent`
block.builders.init.addBlock(deindent`
function ${handlerName} ( event ) {
${handlerBody}
};
@ -137,7 +137,7 @@ export default function visitWindow(
if (bindings.scrollX && bindings.scrollY) {
const observerCallback = block.getUniqueName(`scrollobserver`);
block.builders.create.addBlock(deindent`
block.builders.init.addBlock(deindent`
function ${observerCallback} () {
if ( ${lock} ) return;
var x = ${bindings.scrollX
@ -151,17 +151,17 @@ export default function visitWindow(
`);
if (bindings.scrollX)
block.builders.create.addLine(
block.builders.init.addLine(
`${block.component}.observe( '${bindings.scrollX}', ${observerCallback} );`
);
if (bindings.scrollY)
block.builders.create.addLine(
block.builders.init.addLine(
`${block.component}.observe( '${bindings.scrollY}', ${observerCallback} );`
);
} else if (bindings.scrollX || bindings.scrollY) {
const isX = !!bindings.scrollX;
block.builders.create.addBlock(deindent`
block.builders.init.addBlock(deindent`
${block.component}.observe( '${bindings.scrollX ||
bindings.scrollY}', function ( ${isX ? 'x' : 'y'} ) {
if ( ${lock} ) return;
@ -173,7 +173,7 @@ export default function visitWindow(
// another special case. (I'm starting to think these are all special cases.)
if (bindings.online) {
const handlerName = block.getUniqueName(`onlinestatuschanged`);
block.builders.create.addBlock(deindent`
block.builders.init.addBlock(deindent`
function ${handlerName} ( event ) {
${block.component}.set({ ${bindings.online}: navigator.onLine });
};

@ -105,10 +105,19 @@ export default function visitIfBlock(
simple(generator, block, state, node, branches[0], dynamic, vars);
}
block.builders.create.addLine(
`${if_name}${name}.create();`
);
block.builders.claim.addLine(
`${if_name}${name}.claim( ${state.parentNodes} );`
);
if (node.needsAnchor) {
block.addElement(
anchor,
`${generator.helper('createComment')}()`,
`${generator.helper('createComment')}()`,
state.parentNode,
true
);
@ -126,22 +135,18 @@ function simple(
dynamic,
{ name, anchor, params, if_name }
) {
block.builders.create.addBlock(deindent`
block.builders.init.addBlock(deindent`
var ${name} = (${branch.condition}) && ${branch.block}( ${params}, ${block.component} );
`);
const isTopLevel = !state.parentNode;
const mountOrIntro = branch.hasIntroMethod ? 'intro' : 'mount';
const targetNode = state.parentNode || block.target;
const anchorNode = state.parentNode ? 'null' : 'anchor';
if (isTopLevel) {
block.builders.mount.addLine(
`if ( ${name} ) ${name}.${mountOrIntro}( ${block.target}, anchor );`
);
} else {
block.builders.create.addLine(
`if ( ${name} ) ${name}.${mountOrIntro}( ${state.parentNode}, null );`
);
}
block.builders.mount.addLine(
`if ( ${name} ) ${name}.${mountOrIntro}( ${targetNode}, ${anchorNode} );`
);
const parentNode = state.parentNode || `${anchor}.parentNode`;
@ -152,6 +157,7 @@ function simple(
${name}.update( changed, ${params} );
} else {
${name} = ${branch.block}( ${params}, ${block.component} );
if ( ${name} ) ${name}.create();
}
${name}.intro( ${parentNode}, ${anchor} );
@ -161,17 +167,20 @@ function simple(
${name}.update( changed, ${params} );
} else {
${name} = ${branch.block}( ${params}, ${block.component} );
${name}.create();
${name}.mount( ${parentNode}, ${anchor} );
}
`
: branch.hasIntroMethod
? deindent`
if ( !${name} ) ${name} = ${branch.block}( ${params}, ${block.component} );
${name}.create();
${name}.intro( ${parentNode}, ${anchor} );
`
: deindent`
if ( !${name} ) {
${name} = ${branch.block}( ${params}, ${block.component} );
${name}.create();
${name}.mount( ${parentNode}, ${anchor} );
}
`;
@ -218,7 +227,7 @@ function compound(
const current_block = block.getUniqueName(`current_block`);
const current_block_and = hasElse ? '' : `${current_block} && `;
block.builders.create.addBlock(deindent`
block.builders.init.addBlock(deindent`
function ${get_block} ( ${params} ) {
${branches
.map(({ condition, block }) => {
@ -234,24 +243,27 @@ function compound(
const isTopLevel = !state.parentNode;
const mountOrIntro = branches[0].hasIntroMethod ? 'intro' : 'mount';
if (isTopLevel) {
block.builders.mount.addLine(
`${if_name}${name}.${mountOrIntro}( ${block.target}, anchor );`
);
} else {
block.builders.create.addLine(
`${if_name}${name}.${mountOrIntro}( ${state.parentNode}, null );`
);
}
const targetNode = state.parentNode || block.target;
const anchorNode = state.parentNode ? 'null' : 'anchor';
block.builders.mount.addLine(
`${if_name}${name}.${mountOrIntro}( ${targetNode}, ${anchorNode} );`
);
const parentNode = state.parentNode || `${anchor}.parentNode`;
const changeBlock = deindent`
${if_name}{
${name}.unmount();
${name}.destroy();
}
${hasElse ?
deindent`
${name}.unmount();
${name}.destroy();
` :
deindent`
if ( ${name} ) {
${name}.unmount();
${name}.destroy();
}`}
${name} = ${current_block_and}${current_block}( ${params}, ${block.component} );
${if_name}${name}.create();
${if_name}${name}.${mountOrIntro}( ${parentNode}, ${anchor} );
`;
@ -304,7 +316,7 @@ function compoundWithOutros(
block.addVariable(current_block_index);
block.addVariable(name);
block.builders.create.addBlock(deindent`
block.builders.init.addBlock(deindent`
var ${if_block_creators} = [
${branches.map(branch => branch.block).join(',\n')}
];
@ -323,12 +335,12 @@ function compoundWithOutros(
`);
if (hasElse) {
block.builders.create.addBlock(deindent`
block.builders.init.addBlock(deindent`
${current_block_index} = ${get_block}( ${params} );
${name} = ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, ${block.component} );
`);
} else {
block.builders.create.addBlock(deindent`
block.builders.init.addBlock(deindent`
if ( ~( ${current_block_index} = ${get_block}( ${params} ) ) ) {
${name} = ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, ${block.component} );
}
@ -337,16 +349,12 @@ function compoundWithOutros(
const isTopLevel = !state.parentNode;
const mountOrIntro = branches[0].hasIntroMethod ? 'intro' : 'mount';
const targetNode = state.parentNode || block.target;
const anchorNode = state.parentNode ? 'null' : 'anchor';
if (isTopLevel) {
block.builders.mount.addLine(
`${if_current_block_index}${if_blocks}[ ${current_block_index} ].${mountOrIntro}( ${block.target}, anchor );`
);
} else {
block.builders.create.addLine(
`${if_current_block_index}${if_blocks}[ ${current_block_index} ].${mountOrIntro}( ${state.parentNode}, null );`
);
}
block.builders.mount.addLine(
`${if_current_block_index}${if_blocks}[ ${current_block_index} ].${mountOrIntro}( ${targetNode}, ${anchorNode} );`
);
const parentNode = state.parentNode || `${anchor}.parentNode`;
@ -360,6 +368,7 @@ function compoundWithOutros(
const createNewBlock = deindent`
${name} = ${if_blocks}[ ${current_block_index} ] = ${if_block_creators}[ ${current_block_index} ]( ${params}, ${block.component} );
${name}.create();
${name}.${mountOrIntro}( ${parentNode}, ${anchor} );
`;

@ -19,6 +19,7 @@ export default function visitMustacheTag(
block.addElement(
name,
`${generator.helper('createText')}( ${value} = ${snippet} )`,
generator.hydratable ? `${generator.helper('claimText')}( ${state.parentNodes}, ${value} = ${snippet} )` : '',
state.parentNode,
true
);

@ -17,34 +17,33 @@ export default function visitRawMustacheTag(
const { snippet } = block.contextualise(node.expression);
block.addVariable(value);
// we would have used comments here, but the `insertAdjacentHTML` api only
// exists for `Element`s.
block.addElement(
before,
`${generator.helper('createElement')}( 'noscript' )`,
`${generator.helper('createElement')}( 'noscript' )`,
state.parentNode,
true
);
block.addElement(
after,
`${generator.helper('createElement')}( 'noscript' )`,
`${generator.helper('createElement')}( 'noscript' )`,
state.parentNode,
true
);
const isToplevel = !state.parentNode;
block.builders.create.addLine(`var ${value} = ${snippet};`);
const mountStatement = `${before}.insertAdjacentHTML( 'afterend', ${value} );`;
const mountStatement = `${before}.insertAdjacentHTML( 'afterend', ${value} = ${snippet} );`;
const detachStatement = `${generator.helper(
'detachBetween'
)}( ${before}, ${after} );`;
if (isToplevel) {
block.builders.mount.addLine(mountStatement);
} else {
block.builders.create.addLine(mountStatement);
}
block.builders.mount.addLine(mountStatement);
block.builders.update.addBlock(deindent`
if ( ${value} !== ( ${value} = ${snippet} ) ) {

@ -13,6 +13,7 @@ export default function visitText(
block.addElement(
node._state.name,
`${generator.helper('createText')}( ${JSON.stringify(node.data)} )`,
generator.hydratable ? `${generator.helper('claimText')}( ${state.parentNodes}, ${JSON.stringify(node.data)} )` : '',
state.parentNode,
node.usedAsAnchor
);

@ -9,7 +9,7 @@ export default function visitYieldTag(
) {
const parentNode = state.parentNode || block.target;
(state.parentNode ? block.builders.create : block.builders.mount).addLine(
block.builders.mount.addLine(
`if ( ${block.component}._yield ) ${block.component}._yield.mount( ${parentNode}, null );`
);

@ -41,6 +41,7 @@ export interface CompileOptions {
dev?: boolean;
shared?: boolean | string;
cascade?: boolean;
hydratable?: boolean;
onerror?: (error: Error) => void;
onwarn?: (warning: Warning) => void;

@ -66,3 +66,34 @@ export function getBindingGroupValue(group) {
export function toNumber(value) {
return value === '' ? undefined : +value;
}
export function children (element) {
return Array.from(element.childNodes);
}
export function claimElement (nodes, name, attributes, svg) {
for (var i = 0; i < nodes.length; i += 1) {
var node = nodes[i];
if (node.nodeName === name) {
for (var j = 0; j < node.attributes.length; j += 1) {
var attribute = node.attributes[j];
if (!attributes[attribute.name]) node.removeAttribute(attribute.name);
}
return nodes.splice(i, 1)[0]; // TODO strip unwanted attributes
}
}
return svg ? createSvgElement(name) : createElement(name);
}
export function claimText (nodes, data) {
for (var i = 0; i < nodes.length; i += 1) {
var node = nodes[i];
if (node.nodeType === 3) {
node.data = data;
return nodes.splice(i, 1)[0];
}
}
return createText(data);
}

@ -167,8 +167,9 @@ function capitalize(str) {
return str[0].toUpperCase() + str.slice(1);
}
export function showOutput(cwd, options) {
export function showOutput(cwd, options = {}) {
glob.sync('**/*.html', { cwd }).forEach(file => {
if (file[0] === '_') return;
const { code } = svelte.compile(
fs.readFileSync(`${cwd}/${file}`, 'utf-8'),
Object.assign(options, {

@ -0,0 +1,105 @@
import assert from 'assert';
import path from 'path';
import fs from 'fs';
import {
showOutput,
loadConfig,
loadSvelte,
env,
setupHtmlEqual
} from '../helpers.js';
let compileOptions = null;
function getName(filename) {
const base = path.basename(filename).replace('.html', '');
return base[0].toUpperCase() + base.slice(1);
}
const nodeVersionMatch = /^v(\d)/.exec(process.version);
const legacy = +nodeVersionMatch[1] < 6;
const babelrc = require('../../package.json').babel;
describe('hydration', () => {
before(() => {
const svelte = loadSvelte();
require.extensions['.html'] = function(module, filename) {
const options = Object.assign(
{ filename, name: getName(filename), hydratable: true },
compileOptions
);
let { code } = svelte.compile(fs.readFileSync(filename, 'utf-8'), options);
if (legacy) code = require('babel-core').transform(code, babelrc).code;
return module._compile(code, filename);
};
return setupHtmlEqual();
});
function runTest(dir) {
if (dir[0] === '.') return;
const config = loadConfig(`./hydration/samples/${dir}/_config.js`);
if (config.solo && process.env.CI) {
throw new Error('Forgot to remove `solo: true` from test');
}
(config.skip ? it.skip : config.solo ? it.only : it)(dir, () => {
const cwd = path.resolve(`test/hydration/samples/${dir}`);
compileOptions = config.compileOptions || {};
compileOptions.shared = path.resolve('shared.js');
compileOptions.dev = config.dev;
compileOptions.hydrate = true;
return env()
.then(window => {
global.window = window;
let SvelteComponent;
try {
SvelteComponent = require(`${cwd}/main.html`).default;
} catch (err) {
throw err;
}
const target = window.document.body;
target.innerHTML = fs.readFileSync(`${cwd}/_before.html`, 'utf-8');
const snapshot = config.snapshot ? config.snapshot(target) : {};
const component = new SvelteComponent({
target,
hydrate: true,
data: config.data
});
assert.htmlEqual(target.innerHTML, fs.readFileSync(`${cwd}/_after.html`, 'utf-8'));
if (config.test) {
config.test(assert, target, snapshot, component, window);
} else {
component.destroy();
assert.equal(target.innerHTML, '');
}
})
.catch(err => {
showOutput(cwd, { shared: 'svelte/shared.js' }); // eslint-disable-line no-console
throw err;
})
.then(() => {
if (config.show) showOutput(cwd, { shared: 'svelte/shared.js' });
});
});
}
fs.readdirSync('test/hydration/samples').forEach(dir => {
runTest(dir, null);
});
});

@ -0,0 +1,17 @@
export default {
snapshot(target) {
const h1 = target.querySelector('h1');
return {
h1,
text: h1.childNodes[0]
};
},
test(assert, target, snapshot) {
const h1 = target.querySelector('h1');
assert.equal(h1, snapshot.h1);
assert.equal(h1.childNodes[0], snapshot.text);
}
};

@ -0,0 +1,2 @@
<input>
<p>Hello world!</p>

@ -0,0 +1,2 @@
<input>
<p>Hello world!</p>

@ -0,0 +1,29 @@
export default {
data: {
name: 'world'
},
snapshot(target) {
return {
input: target.querySelector('input'),
p: target.querySelector('p')
};
},
test(assert, target, snapshot, component, window) {
const input = target.querySelector('input');
const p = target.querySelector('p');
assert.equal(input, snapshot.input);
assert.equal(p, snapshot.p);
input.value = 'everybody';
input.dispatchEvent(new window.Event('input'));
assert.equal(component.get('name'), 'everybody');
assert.htmlEqual(target.innerHTML, `
<input>
<p>Hello everybody!</p>
`);
}
};

@ -0,0 +1,2 @@
<input bind:value='name'>
<p>Hello {{name}}!</p>

@ -0,0 +1,21 @@
export default {
snapshot(target) {
const div = target.querySelector('div');
const p = target.querySelector('p');
return {
div,
p,
text: p.childNodes[0]
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
const p = target.querySelector('p');
assert.equal(div, snapshot.div);
assert.equal(p, snapshot.p);
assert.equal(p.childNodes[0], snapshot.text);
}
};

@ -0,0 +1,13 @@
<div>
<Nested/>
</div>
<script>
import Nested from './Nested.html';
export default {
components: {
Nested
}
};
</script>

@ -0,0 +1,17 @@
export default {
snapshot(target) {
const p = target.querySelector('p');
return {
p,
text: p.childNodes[0]
};
},
test(assert, target, snapshot) {
const p = target.querySelector('p');
assert.equal(p, snapshot.p);
assert.equal(p.childNodes[0], snapshot.text);
}
};

@ -0,0 +1,11 @@
<Nested/>
<script>
import Nested from './Nested.html';
export default {
components: {
Nested
}
};
</script>

@ -0,0 +1,21 @@
export default {
data: {
name: 'everybody'
},
snapshot(target) {
const h1 = target.querySelector('h1');
return {
h1,
text: h1.childNodes[0]
};
},
test(assert, target, snapshot) {
const h1 = target.querySelector('h1');
assert.equal(h1, snapshot.h1);
assert.equal(h1.childNodes[0], snapshot.text);
}
};

@ -0,0 +1,21 @@
export default {
data: {
name: 'world'
},
snapshot(target) {
const h1 = target.querySelector('h1');
return {
h1,
text: h1.childNodes[0]
};
},
test(assert, target, snapshot) {
const h1 = target.querySelector('h1');
assert.equal(h1, snapshot.h1);
assert.equal(h1.childNodes[0], snapshot.text);
}
};

@ -0,0 +1,5 @@
<ul>
<li>animal</li>
<li>vegetable</li>
<li>mineral</li>
</ul>

@ -0,0 +1,5 @@
<ul>
<li>animal</li>
<li>vegetable</li>
<li>mineral</li>
</ul>

@ -0,0 +1,29 @@
export default {
data: {
things: [
'animal',
'vegetable',
'mineral'
]
},
snapshot(target) {
const ul = target.querySelector('ul');
const lis = ul.querySelectorAll('li');
return {
ul,
lis
};
},
test(assert, target, snapshot) {
const ul = target.querySelector('ul');
const lis = ul.querySelectorAll('li');
assert.equal(ul, snapshot.ul);
assert.equal(lis[0], snapshot.lis[0]);
assert.equal(lis[1], snapshot.lis[1]);
assert.equal(lis[2], snapshot.lis[2]);
}
};

@ -0,0 +1,5 @@
<ul>
{{#each things as thing}}
<li>{{thing}}</li>
{{/each}}
</ul>

@ -0,0 +1,19 @@
export default {
data: {
class: 'bar'
},
snapshot(target) {
const div = target.querySelector('div');
return {
div
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
}
};

@ -0,0 +1,19 @@
export default {
data: {
class: 'bar'
},
snapshot(target) {
const div = target.querySelector('div');
return {
div
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
}
};

@ -0,0 +1,19 @@
export default {
data: {
class: 'bar'
},
snapshot(target) {
const div = target.querySelector('div');
return {
div
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
}
};

@ -0,0 +1,15 @@
export default {
snapshot(target) {
const div = target.querySelector('div');
return {
div
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
}
};

@ -0,0 +1,17 @@
export default {
snapshot(target) {
const div = target.querySelector('div');
return {
div,
p: div.querySelector('p')
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
assert.equal(div, snapshot.div);
assert.equal(div.querySelector('p'), snapshot.p);
}
};

@ -0,0 +1,3 @@
<div>
<p>nested</p>
</div>

@ -0,0 +1,16 @@
export default {
snapshot(target) {
const h1 = target.querySelector('h1');
return {
h1,
};
},
test(assert, target, snapshot, component) {
const h1 = target.querySelector('h1');
assert.equal(h1, snapshot.h1);
assert.equal(component.refs.h1, h1);
}
};

@ -0,0 +1 @@
<h1 ref:h1>Hello world!</h1>

@ -0,0 +1,26 @@
export default {
data: {
clicked: false
},
snapshot(target) {
const button = target.querySelector('button');
return {
button
};
},
test(assert, target, snapshot, component, window) {
const button = target.querySelector('button');
assert.equal(button, snapshot.button);
button.dispatchEvent(new window.MouseEvent('click'));
assert.ok(component.get('clicked'));
assert.htmlEqual(target.innerHTML, `
<button>click me</button>
<p>clicked!</p>
`);
}
};

@ -0,0 +1,5 @@
<button on:click='set({ clicked: true })'>click me</button>
{{#if clicked}}
<p>clicked!</p>
{{/if}}

@ -0,0 +1,4 @@
<div>
<p>foo!</p>
<p>bar!</p>
</div>

@ -0,0 +1,4 @@
<div>
<p>foo!</p>
<p>bar!</p>
</div>

@ -0,0 +1,26 @@
export default {
data: {
foo: true,
bar: true
},
snapshot(target) {
const div = target.querySelector('div');
const ps = target.querySelectorAll('p');
return {
div,
p0: ps[0],
p1: ps[1]
};
},
test(assert, target, snapshot) {
const div = target.querySelector('div');
const ps = target.querySelectorAll('p');
assert.equal(div, snapshot.div);
assert.equal(ps[0], snapshot.p0);
assert.equal(ps[1], snapshot.p1);
}
};

@ -0,0 +1,9 @@
<div>
{{#if foo}}
<p>foo!</p>
{{/if}}
{{#if bar}}
<p>bar!</p>
{{/if}}
</div>

@ -0,0 +1,19 @@
export default {
data: {
foo: false
},
snapshot(target) {
const p = target.querySelector('p');
return {
p
};
},
test(assert, target, snapshot) {
const p = target.querySelector('p');
assert.equal(p, snapshot.p);
}
};

@ -0,0 +1,5 @@
<p>before</p>
{{#if foo}}
<p>foo!</p>
{{/if}}
<p>after</p>

@ -0,0 +1,23 @@
export default {
data: {
foo: true,
bar: false
},
snapshot(target) {
const p = target.querySelector('p');
return {
p
};
},
test(assert, target, snapshot, component) {
const p = target.querySelector('p');
assert.equal(p, snapshot.p);
component.set({ foo: false, bar: true });
assert.htmlEqual(target.innerHTML, `<p>bar!</p>`);
}
};

@ -0,0 +1,7 @@
{{#if foo}}
<p>foo!</p>
{{/if}}
{{#if bar}}
<p>bar!</p>
{{/if}}

@ -0,0 +1,19 @@
export default {
data: {
foo: true
},
snapshot(target) {
const p = target.querySelector('p');
return {
p
};
},
test(assert, target, snapshot) {
const p = target.querySelector('p');
assert.equal(p, snapshot.p);
}
};

@ -0,0 +1,3 @@
{{#if foo}}
<p>foo!</p>
{{/if}}

@ -0,0 +1,4 @@
<noscript></noscript>
<p>this is some html</p>
<p>and so is this</p>
<noscript></noscript>

@ -0,0 +1,2 @@
<p>this is some html</p>
<p>and so is this</p>

@ -0,0 +1,27 @@
export default {
skip: true, // existing nodes are blown away
data: {
raw: `<p>this is some html</p> <p>and so is this</p>`
},
snapshot(target) {
const ps = target.querySelectorAll('p');
return {
p0: ps[0],
text0: ps[0].firstChild,
p1: ps[1],
text1: ps[1].firstChild
};
},
test(assert, target, snapshot) {
const ps = target.querySelectorAll('p');
assert.equal(ps[0], snapshot.p0);
assert.equal(ps[0].firstChild, snapshot.text0);
assert.equal(ps[1], snapshot.p1);
assert.equal(ps[1].firstChild, snapshot.text1);
}
};

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save