import CodeBuilder from '../../utils/CodeBuilder'; import deindent from '../../utils/deindent'; import { escape } from '../../utils/stringify'; import { DomGenerator } from './index'; import { Node } from '../../interfaces'; import shared from './shared'; export interface BlockOptions { name: string; generator?: DomGenerator; expression?: Node; context?: string; destructuredContexts?: string[]; comment?: string; key?: string; contexts?: Map; contextTypes?: Map; indexes?: Map; changeableIndexes?: Map; indexNames?: Map; listNames?: Map; indexName?: string; listName?: string; dependencies?: Set; } export default class Block { generator: DomGenerator; name: string; expression: Node; context: string; destructuredContexts?: string[]; comment?: string; key: string; first: string; contexts: Map; contextTypes: Map; indexes: Map; changeableIndexes: Map; dependencies: Set; indexNames: Map; listNames: Map; indexName: string; listName: string; builders: { init: CodeBuilder; create: CodeBuilder; claim: CodeBuilder; hydrate: CodeBuilder; mount: CodeBuilder; intro: CodeBuilder; update: CodeBuilder; outro: CodeBuilder; unmount: CodeBuilder; detachRaw: CodeBuilder; destroy: CodeBuilder; }; hasIntroMethod: boolean; hasOutroMethod: boolean; outros: number; aliases: Map; variables: Map; getUniqueName: (name: string) => string; hasUpdateMethod: boolean; autofocus: string; constructor(options: BlockOptions) { this.generator = options.generator; this.name = options.name; this.expression = options.expression; this.context = options.context; this.destructuredContexts = options.destructuredContexts; this.comment = options.comment; // for keyed each blocks this.key = options.key; this.first = null; this.contexts = options.contexts; this.contextTypes = options.contextTypes; this.indexes = options.indexes; this.changeableIndexes = options.changeableIndexes; this.dependencies = new Set(); this.indexNames = options.indexNames; this.listNames = options.listNames; this.indexName = options.indexName; 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(), outro: new CodeBuilder(), unmount: new CodeBuilder(), detachRaw: new CodeBuilder(), destroy: new CodeBuilder(), }; this.hasIntroMethod = false; // a block could have an intro method but not intro transitions, e.g. if a sibling block has intros this.hasOutroMethod = false; this.outros = 0; this.aliases = new Map(); this.variables = new Map(); this.getUniqueName = this.generator.getUniqueNameMaker(); this.hasUpdateMethod = false; // determined later } addDependencies(dependencies: string[]) { dependencies.forEach(dependency => { this.dependencies.add(dependency); }); } addElement( name: string, renderStatement: string, claimStatement: string, parentNode: string ) { this.addVariable(name); this.builders.create.addLine(`${name} = ${renderStatement};`); this.builders.claim.addLine(`${name} = ${claimStatement || renderStatement};`); if (parentNode) { this.builders.mount.addLine(`@appendNode(${name}, ${parentNode});`); if (parentNode === 'document.head') this.builders.unmount.addLine(`@detachNode(${name});`); } else { this.builders.mount.addLine(`@insertNode(${name}, #target, anchor);`); this.builders.unmount.addLine(`@detachNode(${name});`); } } addVariable(name: string, init?: string) { if (this.variables.has(name) && this.variables.get(name) !== init) { throw new Error( `Variable '${name}' already initialised with a different value` ); } this.variables.set(name, init); } alias(name: string) { if (!this.aliases.has(name)) { this.aliases.set(name, this.getUniqueName(name)); } return this.aliases.get(name); } child(options: BlockOptions) { return new Block(Object.assign({}, this, { key: null }, options, { parent: this })); } contextualise(expression: Node, context?: string, isEventHandler?: boolean) { return this.generator.contextualise(this.contexts, this.indexes, expression, context, isEventHandler); } toString() { let introing; const hasIntros = !this.builders.intro.isEmpty(); if (hasIntros) { introing = this.getUniqueName('introing'); this.addVariable(introing); } let outroing; const hasOutros = !this.builders.outro.isEmpty(); if (hasOutros) { outroing = this.alias('outroing'); this.addVariable(outroing); } if (this.autofocus) { this.builders.mount.addLine(`${this.autofocus}.focus();`); } // TODO `this.contexts` is possibly redundant post-#1122 const initializers = []; const updaters = []; this.contexts.forEach((alias, name) => { // TODO only the ones that are actually used in this block... const assignment = `${alias} = state.${name}`; initializers.push(assignment); updaters.push(`${assignment};`); this.hasUpdateMethod = true; }); this.indexNames.forEach((alias, name) => { // TODO only the ones that are actually used in this block... const assignment = `${alias} = state.${alias}`; // TODO this is wrong!!! initializers.push(assignment); updaters.push(`${assignment};`); this.hasUpdateMethod = true; }); // minor hack – we need to ensure that any {{{triples}}} are detached first this.builders.unmount.addBlockAtStart(this.builders.detachRaw.toString()); const properties = new CodeBuilder(); let localKey; if (this.key) { localKey = this.getUniqueName('key'); properties.addBlock(`key: ${localKey},`); } if (this.first) { properties.addBlock(`first: null,`); this.builders.hydrate.addLine(`this.first = ${this.first};`); } if (this.builders.create.isEmpty() && this.builders.hydrate.isEmpty()) { properties.addBlock(`c: @noop,`); } else { properties.addBlock(deindent` c: function create() { ${this.builders.create} ${!this.builders.hydrate.isEmpty() && `this.h();`} }, `); } if (this.generator.hydratable) { if (this.builders.claim.isEmpty() && this.builders.hydrate.isEmpty()) { properties.addBlock(`l: @noop,`); } else { properties.addBlock(deindent` l: function claim(nodes) { ${this.builders.claim} ${!this.builders.hydrate.isEmpty() && `this.h();`} }, `); } } if (!this.builders.hydrate.isEmpty()) { properties.addBlock(deindent` h: function hydrate() { ${this.builders.hydrate} }, `); } if (this.builders.mount.isEmpty()) { properties.addBlock(`m: @noop,`); } else { properties.addBlock(deindent` m: function mount(#target, anchor) { ${this.builders.mount} }, `); } if (this.hasUpdateMethod) { if (this.builders.update.isEmpty() && updaters.length === 0) { properties.addBlock(`p: @noop,`); } else { properties.addBlock(deindent` p: function update(changed, state) { ${updaters} ${this.builders.update} }, `); } } if (this.hasIntroMethod) { if (hasIntros) { properties.addBlock(deindent` i: function intro(#target, anchor) { if (${introing}) return; ${introing} = true; ${hasOutros && `${outroing} = false;`} ${this.builders.intro} this.m(#target, anchor); }, `); } else { properties.addBlock(deindent` i: function intro(#target, anchor) { this.m(#target, anchor); }, `); } } if (this.hasOutroMethod) { if (hasOutros) { properties.addBlock(deindent` o: function outro(${this.alias('outrocallback')}) { if (${outroing}) return; ${outroing} = true; ${hasIntros && `${introing} = false;`} var ${this.alias('outros')} = ${this.outros}; ${this.builders.outro} }, `); } else { // TODO should this be a helper? properties.addBlock(deindent` o: function outro(outrocallback) { outrocallback(); }, `); } } if (this.builders.unmount.isEmpty()) { properties.addBlock(`u: @noop,`); } else { properties.addBlock(deindent` u: function unmount() { ${this.builders.unmount} }, `); } if (this.builders.destroy.isEmpty()) { properties.addBlock(`d: @noop`); } else { properties.addBlock(deindent` d: function destroy() { ${this.builders.destroy} } `); } return deindent` ${this.comment && `// ${escape(this.comment)}`} function ${this.name}(#component${this.key ? `, ${localKey}` : ''}, state) { ${initializers.length > 0 && `var ${initializers.join(', ')};`} ${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} }; } `.replace(/(#+)(\w*)/g, (match: string, sigil: string, name: string) => { return sigil === '#' ? this.alias(name) : sigil.slice(1) + name; }); } }