diff --git a/src/generators/nodes/EachBlock.ts b/src/generators/nodes/EachBlock.ts index 6aa9ab673e..ba9ffc11a9 100644 --- a/src/generators/nodes/EachBlock.ts +++ b/src/generators/nodes/EachBlock.ts @@ -315,108 +315,16 @@ export default class EachBlock extends Node { `); const dynamic = this.block.hasUpdateMethod; - let fn_destroy; - if (this.block.hasOutroMethod) { - fn_destroy = block.getUniqueName(`${each}_outro`); - block.builders.init.addBlock(deindent` - function ${fn_destroy}(iteration) { - iteration.o(function() { - iteration.u(); - iteration.d(); - ${lookup}[iteration.key] = null; - }); - } - `); - } else { - fn_destroy = block.getUniqueName(`${each}_destroy`); - block.builders.init.addBlock(deindent` - function ${fn_destroy}(iteration) { - var first = iteration.first - if (first && first.parentNode) { - iteration.u(); - } - iteration.d(); - ${lookup}[iteration.key] = null; - } - `); - } - const destroy = deindent` - ${iteration} = ${head}; - while(${iteration}) { - if (!${keep}[${iteration}.key]) { - ${fn_destroy}(${iteration}); - } - ${iteration} = ${iteration}.next; - } - `; block.builders.update.addBlock(deindent` var ${each_block_value} = ${snippet}; - var ${expected} = ${head}; - var ${last} = null; - - var ${keep} = {}; - var ${mounts} = {}; - var ${next_iteration} = null; - for (#i = 0; #i < ${each_block_value}.${length}; #i += 1) { - var ${key} = ${each_block_value}[#i].${this.key}; - var ${iteration} = ${lookup}[${key}]; - var next_data = ${each_block_value}[#i+1]; - var next = next_data && ${lookup}[next_data.${this.key}]; - - var ${this.each_context} = @assign({}, state, { + @updateKeyedEach(#component, ${key}, changed, "${this.key}", ${dynamic}, ${each_block_value}, ${head}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", function(#i) { + return @assign({}, state, { ${this.contextProps.join(',\n')} }); + }); - ${dynamic && - `if (${iteration}) ${iteration}.p(changed, ${this.each_context});`} - if (${expected} && (${key} === ${expected}.key)) { - var first = ${iteration} && ${iteration}.first; - var parentNode = first && first.parentNode - if (!parentNode || (${iteration} && ${iteration}.next) != next) ${mounts}[${key}] = ${iteration}; - ${expected} = ${iteration}.next; - } else if (${iteration}) { - ${mounts}[${key}] = ${iteration}; - ${expected} = ${iteration}.next; - } else { - // key is being inserted - ${iteration} = ${lookup}[${key}] = ${create_each_block}(#component, ${key}, ${this.each_context}); - ${iteration}.c(); - ${mounts}[${key}] = ${iteration}; - } - ${lookup}[${key}] = ${iteration}; - ${keep}[${iteration}.key] = ${iteration}; - ${last} = ${iteration}; - } - ${destroy} - - // Work backwards due to DOM api having insertBefore - for (#i = ${each_block_value}.${length} - 1; #i >= 0; #i -= 1) { - var data = ${each_block_value}[#i]; - var ${key} = data.${this.key}; - ${iteration} = ${lookup}[${key}]; - if (${mounts}[${key}]) { - var anchor; - ${this.block.hasOutroMethod - ? deindent` - var key_next_iteration = ${next_iteration} && ${next_iteration}.key; - var iteration_anchor = ${iteration}.next; - var key_anchor; - do { - anchor = iteration_anchor && iteration_anchor.first; - iteration_anchor = iteration_anchor && iteration_anchor.next; - key_anchor = iteration_anchor && iteration_anchor.key; - } while(iteration_anchor && key_anchor != key_next_iteration && !${keep}[key_anchor])` - : deindent` - anchor = ${next_iteration} && ${next_iteration}.first; - ` } - ${mounts}[${key}].${mountOrIntro}(${updateMountNode}, anchor); - } - ${iteration}.next = ${next_iteration}; - if (${next_iteration}) ${next_iteration}.last = ${iteration}; - ${next_iteration} = ${iteration}; - } ${head} = ${lookup}[${each_block_value}[0] && ${each_block_value}[0].${this.key}]; `); diff --git a/src/shared/_build.js b/src/shared/_build.js index 0339ba558c..3cc523e7cd 100644 --- a/src/shared/_build.js +++ b/src/shared/_build.js @@ -5,7 +5,7 @@ const acorn = require('acorn'); const declarations = {}; fs.readdirSync(__dirname).forEach(file => { - if (!/^[a-z]+\.js$/.test(file)) return; + if (!/^[a-z\-]+\.js$/.test(file)) return; const source = fs.readFileSync(path.join(__dirname, file), 'utf-8'); const ast = acorn.parse(source, { diff --git a/src/shared/index.js b/src/shared/index.js index ab8a053151..a410e7d9df 100644 --- a/src/shared/index.js +++ b/src/shared/index.js @@ -1,6 +1,7 @@ import { assign } from './utils.js'; import { noop } from './utils.js'; export * from './dom.js'; +export * from './keyed-each.js'; export * from './transitions.js'; export * from './utils.js'; diff --git a/src/shared/keyed-each.js b/src/shared/keyed-each.js new file mode 100644 index 0000000000..384a96f6ed --- /dev/null +++ b/src/shared/keyed-each.js @@ -0,0 +1,80 @@ +import { assign } from './utils.js'; + +export function destroyIteration(iteration, lookup) { + var first = iteration.first + if (first && first.parentNode) { + iteration.u(); + } + iteration.d(); + lookup[iteration.key] = null; +} + +export function outroAndDestroyIteration(iteration, lookup) { + iteration.o(function() { + iteration.u(); + iteration.d(); + lookup[iteration.key] = null; + }); +} + +// TODO is it possible to avoid mounting iterations that are +// already in the right place? +export function updateKeyedEach(component, key, changed, key_prop, dynamic, list, head, lookup, node, has_outro, create_each_block, intro_method, get_context) { + var keep = {}; + + var i = list.length; + while (i--) { + var key = list[i][key_prop]; + var iteration = lookup[key]; + + if (iteration) { + if (dynamic) iteration.p(changed, get_context(i)); + } else { + iteration = lookup[key] = create_each_block(component, key, get_context(i)); + iteration.c(); + } + + lookup[key] = iteration; + keep[key] = 1; + } + + var destroy = has_outro + ? outroAndDestroyIteration + : destroyIteration; + + iteration = head; + while (iteration) { + if (!keep[iteration.key]) destroy(iteration, lookup); + iteration = iteration.next; + } + + var next = null; + + i = list.length; + while (i--) { + key = list[i][key_prop]; + iteration = lookup[key]; + + var anchor; + + if (has_outro) { + var next_key = next && next.key; + var neighbour = iteration.next; + var anchor_key; + + while (neighbour && anchor_key != next_key && !keep[anchor_key]) { + anchor = neighbour && neighbour.first; + neighbour = neighbour.next; + anchor_key = neighbour && neighbour.key; + } + } else { + anchor = next && next.first; + } + + iteration[intro_method](node, anchor); + + iteration.next = next; + if (next) next.last = iteration; + next = iteration; + } +} \ No newline at end of file diff --git a/test/runtime/samples/each-block-keyed-random-permute/_config.js b/test/runtime/samples/each-block-keyed-random-permute/_config.js index 904208c6a9..164257056f 100644 --- a/test/runtime/samples/each-block-keyed-random-permute/_config.js +++ b/test/runtime/samples/each-block-keyed-random-permute/_config.js @@ -1,50 +1,60 @@ -const VALUES = Array.from( 'abcdefghijklmnopqrstuvwxyz' ); +const VALUES = Array.from('abcdefghijklmnopqrstuvwxyz'); -function toObjects ( array ) { - return array.split( '' ).map( x => ({ id: x }) ); +function toObjects(array) { + return array.split('').map(x => ({ id: x })); } -function permute () { +function permute() { const values = VALUES.slice(); const number = Math.floor(Math.random() * VALUES.length); const permuted = []; for (let i = 0; i < number; i++) { - permuted.push( ...values.splice( Math.floor( Math.random() * ( number - i ) ), 1 ) ); + permuted.push( + ...values.splice(Math.floor(Math.random() * (number - i)), 1) + ); } - return permuted.join( '' ); + return permuted.join(''); } export default { data: { - values: toObjects( 'abc' ) + values: toObjects('abc'), }, html: `(a)(b)(c)`, - test ( assert, component, target ) { - function test ( sequence ) { - component.set({ values: toObjects( sequence ) }); - assert.htmlEqual( target.innerHTML, sequence.split( '' ).map( x => `(${x})` ).join( '' ) ); + test(assert, component, target) { + function test(sequence) { + const previous = target.textContent; + const expected = sequence.split('').map(x => `(${x})`).join(''); + component.set({ values: toObjects(sequence) }); + assert.htmlEqual( + target.innerHTML, + expected, + `\n${previous} -> ${expected}\n${target.textContent}` + ); } // first, some fixed tests so that we can debug them - test( 'abc' ); - test( 'abcd' ); - test( 'abecd' ); - test( 'fabecd' ); - test( 'fabed' ); - test( 'beadf' ); - test( 'ghbeadf' ); - test( 'gf' ); - test( 'gc' ); - test( 'g' ); - test( '' ); - test( 'abc' ); - test( 'duqbmineapjhtlofrskcg' ); - test( 'hdnkjougmrvftewsqpailcb' ); + test('abc'); + test('abcd'); + test('abecd'); + test('fabecd'); + test('fabed'); + test('beadf'); + test('ghbeadf'); + test('gf'); + test('gc'); + test('g'); + test(''); + test('abc'); + test('duqbmineapjhtlofrskcg'); + test('hdnkjougmrvftewsqpailcb'); + test('bidhfacge'); + test('kgjnempcboaflidh'); // then, we party - for ( let i = 0; i < 100; i += 1 ) test( permute() ); + for (let i = 0; i < 100; i += 1) test(permute()); } };