import Renderer from '../Renderer'; import Block from '../Block'; import Node from '../../nodes/shared/Node'; import Wrapper from './shared/Wrapper'; import createDebuggingComment from '../../../utils/createDebuggingComment'; import EachBlock from '../../nodes/EachBlock'; import FragmentWrapper from './Fragment'; import deindent from '../../../utils/deindent'; import ElseBlock from '../../nodes/ElseBlock'; class ElseBlockWrapper extends Wrapper { node: ElseBlock; block: Block; fragment: FragmentWrapper; isDynamic: boolean; var = null; constructor( renderer: Renderer, block: Block, parent: Wrapper, node: ElseBlock, stripWhitespace: boolean, nextSibling: Wrapper ) { super(renderer, block, parent, node); this.block = block.child({ comment: createDebuggingComment(node, this.renderer.component), name: this.renderer.component.getUniqueName(`create_else_block`) }); this.fragment = new FragmentWrapper( renderer, this.block, this.node.children, parent, stripWhitespace, nextSibling ); this.isDynamic = this.block.dependencies.size > 0; if (this.isDynamic) { // TODO this can't be right this.block.hasUpdateMethod = true; } } } export default class EachBlockWrapper extends Wrapper { block: Block; node: EachBlock; fragment: FragmentWrapper; else?: ElseBlockWrapper; vars: { anchor: string; create_each_block: string; each_block_value: string; get_each_context: string; iterations: string; length: string; mountOrIntro: string; } contextProps: string[]; indexName: string; var = 'each'; hasBinding = false; constructor( renderer: Renderer, block: Block, parent: Wrapper, node: EachBlock, stripWhitespace: boolean, nextSibling: Wrapper ) { super(renderer, block, parent, node); this.cannotUseInnerHTML(); const { dependencies } = node.expression; block.addDependencies(dependencies); this.block = block.child({ comment: createDebuggingComment(this.node, this.renderer.component), name: renderer.component.getUniqueName('create_each_block'), key: <string>node.key, // TODO... bindings: new Map(block.bindings), contextOwners: new Map(block.contextOwners) }); // TODO this seems messy this.block.hasAnimation = this.node.hasAnimation; this.indexName = this.node.index || renderer.component.getUniqueName(`${this.node.context}_index`); node.contexts.forEach(prop => { this.block.contextOwners.set(prop.key.name, this); // TODO this doesn't feel great this.block.bindings.set(prop.key.name, () => `ctx.${this.vars.each_block_value}[ctx.${this.indexName}]${prop.tail}`); }); if (this.node.index) { this.block.getUniqueName(this.node.index); // this prevents name collisions (#1254) } renderer.blocks.push(this.block); this.fragment = new FragmentWrapper(renderer, this.block, node.children, this, stripWhitespace, nextSibling); if (this.node.else) { this.else = new ElseBlockWrapper( renderer, block, this, this.node.else, stripWhitespace, nextSibling ); renderer.blocks.push(this.else.block); if (this.else.isDynamic) { this.block.addDependencies(this.else.block.dependencies); } } block.addDependencies(this.block.dependencies); this.block.hasUpdateMethod = this.block.dependencies.size > 0; // TODO should this logic be in Block? if (this.block.hasOutros || (this.else && this.else.block.hasOutros)) { block.addOutro(); } } render(block: Block, parentNode: string, parentNodes: string) { if (this.fragment.nodes.length === 0) return; const { renderer } = this; const { component } = renderer; // hack the sourcemap, so that if data is missing the bug // is easy to find let c = this.node.start + 2; while (component.source[c] !== 'e') c += 1; component.code.overwrite(c, c + 4, 'length'); const length = `[✂${c}-${c+4}✂]`; const needsAnchor = this.next ? !this.next.isDomNode() : !parentNode || !this.parent.isDomNode(); this.vars = { anchor: needsAnchor ? block.getUniqueName(`${this.var}_anchor`) : (this.next && this.next.var) || 'null', create_each_block: this.block.name, each_block_value: renderer.component.getUniqueName(`${this.var}_value`), get_each_context: renderer.component.getUniqueName(`get_${this.var}_context`), iterations: block.getUniqueName(`${this.var}_blocks`), length: `[✂${c}-${c+4}✂]`, mountOrIntro: (this.block.hasIntroMethod || this.block.hasOutroMethod) ? 'i' : 'm' }; this.contextProps = this.node.contexts.map(prop => `child_ctx.${prop.key.name} = list[i]${prop.tail};`); if (this.hasBinding) this.contextProps.push(`child_ctx.${this.vars.each_block_value} = list;`); if (this.hasBinding || this.node.index) this.contextProps.push(`child_ctx.${this.indexName} = i;`); const { snippet } = this.node.expression; block.builders.init.addLine(`var ${this.vars.each_block_value} = ${snippet};`); renderer.blocks.push(deindent` function ${this.vars.get_each_context}(ctx, list, i) { const child_ctx = Object.create(ctx); ${this.contextProps} return child_ctx; } `); if (this.node.key) { this.renderKeyed(block, parentNode, parentNodes, snippet); } else { this.renderUnkeyed(block, parentNode, parentNodes, snippet); } if (needsAnchor) { block.addElement( this.vars.anchor, `@createComment()`, parentNodes && `@createComment()`, parentNode ); } if (this.else) { const each_block_else = component.getUniqueName(`${this.var}_else`); const mountOrIntro = (this.else.block.hasIntroMethod || this.else.block.hasOutroMethod) ? 'i' : 'm'; 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.init.addBlock(deindent` if (!${this.vars.each_block_value}.${length}) { ${each_block_else} = ${this.else.block.name}(#component, ctx); ${each_block_else}.c(); } `); block.builders.mount.addBlock(deindent` if (${each_block_else}) { ${each_block_else}.${mountOrIntro}(${parentNode || '#target'}, null); } `); const initialMountNode = parentNode || `${this.vars.anchor}.parentNode`; if (this.else.block.hasUpdateMethod) { block.builders.update.addBlock(deindent` if (!${this.vars.each_block_value}.${length} && ${each_block_else}) { ${each_block_else}.p(changed, ctx); } else if (!${this.vars.each_block_value}.${length}) { ${each_block_else} = ${this.else.block.name}(#component, ctx); ${each_block_else}.c(); ${each_block_else}.${mountOrIntro}(${initialMountNode}, ${this.vars.anchor}); } else if (${each_block_else}) { ${each_block_else}.d(1); ${each_block_else} = null; } `); } else { block.builders.update.addBlock(deindent` if (${this.vars.each_block_value}.${length}) { if (${each_block_else}) { ${each_block_else}.d(1); ${each_block_else} = null; } } else if (!${each_block_else}) { ${each_block_else} = ${this.else.block.name}(#component, ctx); ${each_block_else}.c(); ${each_block_else}.${mountOrIntro}(${initialMountNode}, ${this.vars.anchor}); } `); } block.builders.destroy.addBlock(deindent` if (${each_block_else}) ${each_block_else}.d(${parentNode ? '' : 'detach'}); `); } this.fragment.render(this.block, null, 'nodes'); if (this.else) { this.else.fragment.render(this.else.block, null, 'nodes'); } } renderKeyed( block: Block, parentNode: string, parentNodes: string, snippet: string ) { const { create_each_block, length, anchor, mountOrIntro, } = this.vars; const get_key = block.getUniqueName('get_key'); const blocks = block.getUniqueName(`${this.var}_blocks`); const lookup = block.getUniqueName(`${this.var}_lookup`); block.addVariable(blocks, '[]'); block.addVariable(lookup, `@blankObject()`); if (this.fragment.nodes[0].isDomNode()) { this.block.first = this.fragment.nodes[0].var; } else { this.block.first = this.block.getUniqueName('first'); this.block.addElement( this.block.first, `@createComment()`, parentNodes && `@createComment()`, null ); } block.builders.init.addBlock(deindent` const ${get_key} = ctx => ${this.node.key.snippet}; for (var #i = 0; #i < ${this.vars.each_block_value}.${length}; #i += 1) { let child_ctx = ${this.vars.get_each_context}(ctx, ${this.vars.each_block_value}, #i); let key = ${get_key}(child_ctx); ${blocks}[#i] = ${lookup}[key] = ${create_each_block}(#component, key, child_ctx); } `); const initialMountNode = parentNode || '#target'; const updateMountNode = this.getUpdateMountNode(anchor); const anchorNode = parentNode ? 'null' : 'anchor'; block.builders.create.addBlock(deindent` for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].c(); `); if (parentNodes) { block.builders.claim.addBlock(deindent` for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].l(${parentNodes}); `); } block.builders.mount.addBlock(deindent` for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].${mountOrIntro}(${initialMountNode}, ${anchorNode}); `); const dynamic = this.block.hasUpdateMethod; const rects = block.getUniqueName('rects'); const destroy = this.node.hasAnimation ? `@fixAndOutroAndDestroyBlock` : this.block.hasOutros ? `@outroAndDestroyBlock` : `@destroyBlock`; block.builders.update.addBlock(deindent` const ${this.vars.each_block_value} = ${snippet}; ${this.block.hasOutros && `@groupOutros();`} ${this.node.hasAnimation && `for (let #i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].r();`} ${blocks} = @updateKeyedEach(${blocks}, #component, changed, ${get_key}, ${dynamic ? '1' : '0'}, ctx, ${this.vars.each_block_value}, ${lookup}, ${updateMountNode}, ${destroy}, ${create_each_block}, "${mountOrIntro}", ${anchor}, ${this.vars.get_each_context}); ${this.node.hasAnimation && `for (let #i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].a();`} `); if (this.block.hasOutros && this.renderer.component.options.nestedTransitions) { const countdown = block.getUniqueName('countdown'); block.builders.outro.addBlock(deindent` const ${countdown} = @callAfter(#outrocallback, ${blocks}.length); for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].o(${countdown}); `); } block.builders.destroy.addBlock(deindent` for (#i = 0; #i < ${blocks}.length; #i += 1) ${blocks}[#i].d(${parentNode ? '' : 'detach'}); `); } renderUnkeyed( block: Block, parentNode: string, parentNodes: string, snippet: string ) { const { create_each_block, length, iterations, anchor, mountOrIntro, } = this.vars; block.builders.init.addBlock(deindent` var ${iterations} = []; for (var #i = 0; #i < ${this.vars.each_block_value}.${length}; #i += 1) { ${iterations}[#i] = ${create_each_block}(#component, ${this.vars.get_each_context}(ctx, ${this.vars.each_block_value}, #i)); } `); const initialMountNode = parentNode || '#target'; const updateMountNode = this.getUpdateMountNode(anchor); const anchorNode = parentNode ? 'null' : 'anchor'; block.builders.create.addBlock(deindent` for (var #i = 0; #i < ${iterations}.length; #i += 1) { ${iterations}[#i].c(); } `); if (parentNodes) { block.builders.claim.addBlock(deindent` for (var #i = 0; #i < ${iterations}.length; #i += 1) { ${iterations}[#i].l(${parentNodes}); } `); } block.builders.mount.addBlock(deindent` for (var #i = 0; #i < ${iterations}.length; #i += 1) { ${iterations}[#i].${mountOrIntro}(${initialMountNode}, ${anchorNode}); } `); const allDependencies = new Set(this.block.dependencies); const { dependencies } = this.node.expression; dependencies.forEach((dependency: string) => { allDependencies.add(dependency); }); const outroBlock = this.block.hasOutros && block.getUniqueName('outroBlock') if (outroBlock) { block.builders.init.addBlock(deindent` function ${outroBlock}(i, detach, fn) { if (${iterations}[i]) { ${iterations}[i].o(() => { if (detach) { ${iterations}[i].d(detach); ${iterations}[i] = null; } if (fn) fn(); }); } } `); } // TODO do this for keyed blocks as well const condition = Array.from(allDependencies) .map(dependency => `changed.${dependency}`) .join(' || '); if (condition !== '') { const forLoopBody = this.block.hasUpdateMethod ? (this.block.hasIntros || this.block.hasOutros) ? deindent` if (${iterations}[#i]) { ${iterations}[#i].p(changed, child_ctx); } else { ${iterations}[#i] = ${create_each_block}(#component, child_ctx); ${iterations}[#i].c(); } ${iterations}[#i].i(${updateMountNode}, ${anchor}); ` : deindent` if (${iterations}[#i]) { ${iterations}[#i].p(changed, child_ctx); } else { ${iterations}[#i] = ${create_each_block}(#component, child_ctx); ${iterations}[#i].c(); ${iterations}[#i].m(${updateMountNode}, ${anchor}); } ` : deindent` ${iterations}[#i] = ${create_each_block}(#component, child_ctx); ${iterations}[#i].c(); ${iterations}[#i].${mountOrIntro}(${updateMountNode}, ${anchor}); `; const start = this.block.hasUpdateMethod ? '0' : `${iterations}.length`; let destroy; if (this.block.hasOutros) { destroy = deindent` @groupOutros(); for (; #i < ${iterations}.length; #i += 1) ${outroBlock}(#i, 1); `; } else { destroy = deindent` for (${this.block.hasUpdateMethod ? `` : `#i = ${this.vars.each_block_value}.${length}`}; #i < ${iterations}.length; #i += 1) { ${iterations}[#i].d(1); } ${iterations}.length = ${this.vars.each_block_value}.${length}; `; } block.builders.update.addBlock(deindent` if (${condition}) { ${this.vars.each_block_value} = ${snippet}; for (var #i = ${start}; #i < ${this.vars.each_block_value}.${length}; #i += 1) { const child_ctx = ${this.vars.get_each_context}(ctx, ${this.vars.each_block_value}, #i); ${forLoopBody} } ${destroy} } `); } if (outroBlock && this.renderer.component.options.nestedTransitions) { const countdown = block.getUniqueName('countdown'); block.builders.outro.addBlock(deindent` ${iterations} = ${iterations}.filter(Boolean); const ${countdown} = @callAfter(#outrocallback, ${iterations}.length); for (let #i = 0; #i < ${iterations}.length; #i += 1) ${outroBlock}(#i, 0, ${countdown});` ); } block.builders.destroy.addBlock(`@destroyEach(${iterations}, detach);`); } remount(name: string) { // TODO consider keyed blocks return `for (var #i = 0; #i < ${this.vars.iterations}.length; #i += 1) ${this.vars.iterations}[#i].m(${name}._slotted.default, null);`; } }