more hydration

pull/649/head
Rich Harris 7 years ago
parent 14fe89eae8
commit 0bbd917442

@ -42,6 +42,7 @@ export default class Block {
builders: {
init: CodeBuilder;
create: CodeBuilder;
claim: CodeBuilder;
hydrate: CodeBuilder;
mount: CodeBuilder;
intro: CodeBuilder;
@ -90,6 +91,7 @@ export default class Block {
this.builders = {
init: new CodeBuilder(),
create: new CodeBuilder(),
claim: new CodeBuilder(),
hydrate: new CodeBuilder(),
mount: new CodeBuilder(),
intro: new CodeBuilder(),
@ -124,7 +126,7 @@ export default class Block {
addElement(
name: string,
renderStatement: string,
hydrateStatement: string,
claimStatement: string,
parentNode: string,
needsIdentifier = false
) {
@ -132,7 +134,7 @@ export default class Block {
this.addVariable(name);
this.builders.create.addLine(`${name} = ${renderStatement};`);
this.builders.hydrate.addLine(`${name} = ${hydrateStatement};`)
this.builders.claim.addLine(`${name} = ${claimStatement};`)
this.mount(name, parentNode);
@ -235,13 +237,23 @@ export default class Block {
properties.addBlock(deindent`
create: function () {
${this.builders.create}
${!this.builders.hydrate.isEmpty() && `this.hydrate();`}
},
`);
}
if (this.builders.hydrate.isEmpty()) {
properties.addBlock(`hydrate: ${this.generator.helper('noop')},`);
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}

@ -234,8 +234,14 @@ export default function dom(
this._fragment = ${generator.alias(
'create_main_fragment'
)}( this._state, this );
this._fragment.hydrate( ${generator.helper('children')}( options.target ) );
this._fragment.mount( options.target, null );
if ( options.target ) {
var nodes = ${generator.helper('children')}( options.target );
this._fragment.claim( nodes );
nodes.forEach( ${generator.helper('detachNode')} );
this._fragment.mount( options.target, null );
}
${generator.hasComplexBindings &&
`while ( this._bindings.length ) this._bindings.pop()();`}
${(generator.hasComponents || generator.hasIntroTransitions) &&

@ -129,7 +129,7 @@ export default function visitComponent(
const yieldFragment = block.getUniqueName(`${name}_yield_fragment`);
block.builders.create.addLine(
block.builders.init.addLine(
`var ${yieldFragment} = ${childBlock.name}( ${params}, ${block.component} );`
);
@ -222,6 +222,10 @@ 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);
block.builders.create.addLine(`${name}._fragment.create();`);
block.builders.claim.addLine(`${name}._fragment.claim( ${state.parentNodes} );`);
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,7 @@ 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}.${mountOrIntro}( ${parentNode}, ${anchor} );
} else if ( ${each_block_else} ) {
${each_block_else}.unmount();
@ -162,6 +157,7 @@ function keyed(
node._block.addElement(
node._block.first,
`${generator.helper('createComment')}()`,
`${generator.helper('createComment')}()`,
null,
true
);
@ -176,8 +172,6 @@ function keyed(
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();
@ -353,27 +364,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);
});

@ -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);
}
}

@ -51,9 +51,9 @@ export default function visitElement(
block.addVariable(name);
block.builders.create.addLine(`${name} = ${getRenderStatement(generator, childState.namespace, node.name)};`);
block.builders.hydrate.addBlock(deindent`
${name} = ${getHydrateStatement(generator, childState.namespace, state.parentNodes, node.name)};
var ${childState.parentNodes} = ${generator.helper('children')}( ${name} )
block.builders.claim.addBlock(deindent`
${name} = ${getClaimStatement(generator, childState.namespace, state.parentNodes, node)};
var ${childState.parentNodes} = ${generator.helper('children')}( ${name} );
`);
if (state.parentNode) {
@ -62,12 +62,6 @@ export default function visitElement(
block.builders.mount.addLine(`${block.generator.helper('insertNode')}( ${name}, ${block.target}, anchor );`);
}
if (isToplevel) {
block.builders.unmount.addLine(
`${block.generator.helper('detachNode')}( ${name} );`
);
}
// block.addVariable(name);
// block.builders.create.addLine(
@ -78,8 +72,8 @@ export default function visitElement(
// )};`
// );
// block.builders.hydrate.addLine(
// `${name} = ${getHydrateStatement(
// block.builders.claim.addLine(
// `${name} = ${getClaimStatement(
// generator,
// childState.namespace,
// node.name
@ -88,7 +82,7 @@ export default function visitElement(
// 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}', '' );`
@ -150,7 +144,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(
@ -206,6 +200,10 @@ export default function visitElement(
if (node.initialUpdate) {
block.builders.create.addBlock(node.initialUpdate);
}
block.builders.claim.addLine(
`${childState.parentNodes}.forEach( ${generator.helper('detachNode')} );`
);
}
function getRenderStatement(
@ -224,19 +222,24 @@ function getRenderStatement(
return `${generator.helper('createElement')}( '${name}' )`;
}
function getHydrateStatement(
function getClaimStatement(
generator: DomGenerator,
namespace: string,
nodes: string,
name: string
node: Node
) {
if (namespace === 'http://www.w3.org/2000/svg') {
return `${generator.helper('claimSvgElement')}( '${name}' )`;
return `${generator.helper('claimSvgElement')}( '${node.name}' )`;
}
if (namespace) {
throw new Error('TODO hydrate exotic namespaces');
throw new Error('TODO claim exotic namespaces');
}
return `${generator.helper('claimElement')}( ${nodes}, '${name.toUpperCase()}' )`;
const attributes = node.attributes
.filter((attr: Node) => attr.type === 'Attribute')
.map((attr: Node) => `${attr.name}: true`)
.join(', ');
return `${generator.helper('claimElement')}( ${nodes}, '${node.name.toUpperCase()}', ${attributes ? `{ ${attributes} }` : `{}`} )`;
}

@ -109,8 +109,8 @@ export default function visitIfBlock(
`${name}.create();`
);
block.builders.hydrate.addLine(
`${name}.hydrate( ${state.parentNodes} );`
block.builders.claim.addLine(
`${name}.claim( ${state.parentNodes} );`
);
if (node.needsAnchor) {

@ -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} ) ) {

@ -67,14 +67,18 @@ export function toNumber(value) {
return value === '' ? undefined : +value;
}
export function children ( element ) {
export function children (element) {
return Array.from(element.childNodes);
}
export function claimElement ( nodes, name ) {
export function claimElement (nodes, name, attributes) {
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
}
}
@ -83,7 +87,7 @@ export function claimElement ( nodes, name ) {
return createElement(name);
}
export function claimText ( nodes, data ) {
export function claimText (nodes, data) {
for (var i = 0; i < nodes.length; i += 1) {
var node = nodes[i];
if (node.nodeType === 3) {

@ -82,7 +82,7 @@ describe.only('hydration', () => {
assert.htmlEqual(target.innerHTML, fs.readFileSync(`${cwd}/_after.html`, 'utf-8'));
if (config.test) {
config.test(assert, target, snapshot);
config.test(assert, target, snapshot, component);
} else {
component.destroy();
assert.equal(target.innerHTML, '');

@ -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,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,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,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);
}
};
Loading…
Cancel
Save