diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index 2c31f5f68f..e9b7805dfc 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -715,6 +715,9 @@ export default class Generator { if (node.type === 'Element' && (node.name === ':Component' || node.name === ':Self' || generator.components.has(node.name))) { node.type = 'Component'; node.__proto__ = nodes.Component.prototype; + } else if (node.name === ':Window') { // TODO do this in parse? + node.type = 'Window'; + node.__proto__ = nodes.Window.prototype; } else if (node.type in nodes) { node.__proto__ = nodes[node.type].prototype; } diff --git a/src/generators/dom/State.ts b/src/generators/dom/State.ts index eb9a41a5d1..23eed6677f 100644 --- a/src/generators/dom/State.ts +++ b/src/generators/dom/State.ts @@ -25,7 +25,7 @@ export default class State { assign(this, data) } - child(data: StateData) { + child(data?: StateData) { return new State(assign({}, this, { parentNode: null, parentNodes: 'nodes' diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts index a2e3bb281f..5fec3a41ee 100644 --- a/src/generators/dom/index.ts +++ b/src/generators/dom/index.ts @@ -103,7 +103,7 @@ export default function dom( // parsed.html.children.forEach((node: Node) => { // visit(generator, block, state, node, [], []); // }); - parsed.html.build(); + parsed.html.build(block, state); const builder = new CodeBuilder(); const computationBuilder = new CodeBuilder(); diff --git a/src/generators/dom/visitors/shared/Tag.ts b/src/generators/dom/visitors/shared/Tag.ts index 654c2e7cbf..cc30e570b3 100644 --- a/src/generators/dom/visitors/shared/Tag.ts +++ b/src/generators/dom/visitors/shared/Tag.ts @@ -2,7 +2,7 @@ import deindent from '../../../../utils/deindent'; import { DomGenerator } from '../../index'; import Block from '../../Block'; import { Node } from '../../../../interfaces'; -import { State } from '../../interfaces'; +import State from '../../State'; export default function visitTag( generator: DomGenerator, diff --git a/src/generators/nodes/AwaitBlock.ts b/src/generators/nodes/AwaitBlock.ts index 0b011a4017..b95b90f102 100644 --- a/src/generators/nodes/AwaitBlock.ts +++ b/src/generators/nodes/AwaitBlock.ts @@ -1,10 +1,11 @@ import Node from './shared/Node'; import { DomGenerator } from '../dom/index'; import Block from '../dom/Block'; +import visitAwaitBlock from '../dom/visitors/AwaitBlock'; import PendingBlock from './PendingBlock'; import ThenBlock from './ThenBlock'; import CatchBlock from './CatchBlock'; -import { State } from '../dom/interfaces'; +import State from '../dom/State'; import createDebuggingComment from '../../utils/createDebuggingComment'; export default class AwaitBlock extends Node { @@ -65,4 +66,13 @@ export default class AwaitBlock extends Node { this.then._block.hasUpdateMethod = dynamic; this.catch._block.hasUpdateMethod = dynamic; } + + build( + block: Block, + state: State, + elementStack: Node[], + componentStack: Node[] + ) { + visitAwaitBlock(this.generator, block, state, this, elementStack, componentStack); + } } \ No newline at end of file diff --git a/src/generators/nodes/Component.ts b/src/generators/nodes/Component.ts index ef889fa63d..06878d205a 100644 --- a/src/generators/nodes/Component.ts +++ b/src/generators/nodes/Component.ts @@ -1,6 +1,7 @@ import Node from './shared/Node'; import Block from '../dom/Block'; import State from '../dom/State'; +import visitComponent from '../dom/visitors/Component'; export default class Component extends Node { type: 'Component'; // TODO fix this? @@ -58,4 +59,13 @@ export default class Component extends Node { }); } } + + build( + block: Block, + state: State, + elementStack: Node[], + componentStack: Node[] + ) { + visitComponent(this.generator, block, state, this, elementStack, componentStack); + } } \ No newline at end of file diff --git a/src/generators/nodes/EachBlock.ts b/src/generators/nodes/EachBlock.ts index 5d042f4ee6..3f61dc7e41 100644 --- a/src/generators/nodes/EachBlock.ts +++ b/src/generators/nodes/EachBlock.ts @@ -3,6 +3,7 @@ import ElseBlock from './ElseBlock'; import { DomGenerator } from '../dom/index'; import Block from '../dom/Block'; import State from '../dom/State'; +import visitEachBlock from '../dom/visitors/EachBlock'; import createDebuggingComment from '../../utils/createDebuggingComment'; export default class EachBlock extends Node { @@ -112,4 +113,13 @@ export default class EachBlock extends Node { this.else._block.hasUpdateMethod = this.else._block.dependencies.size > 0; } } + + build( + block: Block, + state: State, + elementStack: Node[], + componentStack: Node[] + ) { + visitEachBlock(this.generator, block, state, this, elementStack, componentStack); + } } \ No newline at end of file diff --git a/src/generators/nodes/Element.ts b/src/generators/nodes/Element.ts index f659dbd50f..3fc2783c89 100644 --- a/src/generators/nodes/Element.ts +++ b/src/generators/nodes/Element.ts @@ -1,12 +1,20 @@ +import deindent from '../../utils/deindent'; +import { stringify } from '../../utils/stringify'; +import flattenReference from '../../utils/flattenReference'; +import isVoidElementName from '../../utils/isVoidElementName'; +import validCalleeObjects from '../../utils/validCalleeObjects'; +import reservedNames from '../../utils/reservedNames'; import Node from './shared/Node'; import Block from '../dom/Block'; import State from '../dom/State'; import Attribute from './Attribute'; import * as namespaces from '../../utils/namespaces'; -const meta: Record = { - ':Window': {}, // TODO this should be dealt with in walkTemplate -}; +// temp - move this logic in here +import addBindings from '../dom/visitors/Element/addBindings'; +import addTransitions from '../dom/visitors/Element/addTransitions'; +import visitAttribute from '../dom/visitors/Element/Attribute'; +import visitSlot from '../dom/visitors/Slot'; export default class Element extends Node { type: 'Element'; @@ -141,17 +149,14 @@ export default class Element extends Node { build( block: Block, state: State, - node: Node, elementStack: Node[], componentStack: Node[] ) { - if (this.name in meta) { - return meta[this.name](generator, block, this); - } + const { generator } = this; if (this.name === 'slot') { // TODO deal with in walkTemplate if (this.generator.customElement) { - const slotName = getStaticAttributeValue(this, 'name') || 'default'; + const slotName = this.getStaticAttributeValue('name') || 'default'; this.generator.slots.add(slotName); } else { return visitSlot(this.generator, block, state, this, elementStack, componentStack); @@ -177,7 +182,7 @@ export default class Element extends Node { if (this.generator.hydratable) { block.builders.claim.addBlock(deindent` - ${name} = ${getClaimStatement(generator, childState.namespace, state.parentNodes, node)}; + ${name} = ${getClaimStatement(generator, childState.namespace, state.parentNodes, this)}; var ${childState.parentNodes} = @children(${name}); `); } @@ -195,8 +200,8 @@ export default class Element extends Node { } // add CSS encapsulation attribute - if (this._needsCssAttribute && !generator.customElement) { - generator.needsEncapsulateHelper = true; + if (this._needsCssAttribute && !this.generator.customElement) { + this.generator.needsEncapsulateHelper = true; block.builders.hydrate.addLine( `@encapsulateStyles(${name});` ); @@ -235,32 +240,32 @@ export default class Element extends Node { } } else { this.children.forEach((child: Node) => { - visit(generator, block, childState, child, elementStack.concat(this), componentStack); + child.build(block, childState, elementStack.concat(this), componentStack); }); } - addBindings(generator, block, childState, this); + addBindings(this.generator, block, childState, this); this.attributes.filter((a: Node) => a.type === 'Attribute').forEach((attribute: Node) => { - visitAttribute(generator, block, childState, this, attribute); + visitAttribute(this.generator, block, childState, this, attribute); }); // event handlers this.attributes.filter((a: Node) => a.type === 'EventHandler').forEach((attribute: Node) => { - const isCustomEvent = this.generator.events.has(attribute.name); + const isCustomEvent = generator.events.has(attribute.name); const shouldHoist = !isCustomEvent && state.inEachBlock; const context = shouldHoist ? null : name; const usedContexts: string[] = []; if (attribute.expression) { - this.generator.addSourcemapLocations(attribute.expression); + generator.addSourcemapLocations(attribute.expression); const flattened = flattenReference(attribute.expression.callee); if (!validCalleeObjects.has(flattened.name)) { // allow event.stopPropagation(), this.select() etc // TODO verify that it's a valid callee (i.e. built-in or declared method) - this.generator.code.prependRight( + generator.code.prependRight( attribute.expression.start, `${block.alias('component')}.` ); @@ -294,7 +299,7 @@ export default class Element extends Node { // get a name for the event handler that is globally unique // if hoisted, locally unique otherwise - const handlerName = (shouldHoist ? generator : block).getUniqueName( + const handlerName = (shouldHoist ? this.generator : block).getUniqueName( `${attribute.name.replace(/[^a-zA-Z0-9_$]/g, '_')}_handler` ); @@ -344,7 +349,7 @@ export default class Element extends Node { }); // refs - this.node.attributes.filter((a: Node) => a.type === 'Ref').forEach((attribute: Node) => { + this.attributes.filter((a: Node) => a.type === 'Ref').forEach((attribute: Node) => { const ref = `#component.refs.${attribute.name}`; block.builders.mount.addLine( @@ -358,7 +363,7 @@ export default class Element extends Node { this.generator.usesRefs = true; // so component.refs object is created }); - addTransitions(this.generator, block, childState, node); + addTransitions(this.generator, block, childState, this); if (childState.allUsedContexts.length || childState.usesComponent) { const initialProps: string[] = []; diff --git a/src/generators/nodes/ElseBlock.ts b/src/generators/nodes/ElseBlock.ts index 71b811d23b..85705c5320 100644 --- a/src/generators/nodes/ElseBlock.ts +++ b/src/generators/nodes/ElseBlock.ts @@ -1,5 +1,11 @@ import Node from './shared/Node'; +import Block from '../dom/Block'; +import State from '../dom/State'; export default class ElseBlock extends Node { + type: 'ElseBlock'; + children: Node[]; + _block: Block; + _state: State; } \ No newline at end of file diff --git a/src/generators/nodes/Fragment.ts b/src/generators/nodes/Fragment.ts index 8de7f98129..277b803242 100644 --- a/src/generators/nodes/Fragment.ts +++ b/src/generators/nodes/Fragment.ts @@ -40,15 +40,11 @@ export default class Fragment extends Node { } build( - generator: DomGenerator, block: Block, - state: State, - node: Node, - elementStack: Node[], - componentStack: Node[] + state: State ) { this.children.forEach(child => { - child.build(block, state, node, elementStack, componentStack); + child.build(block, state, [], []); }); } } \ No newline at end of file diff --git a/src/generators/nodes/IfBlock.ts b/src/generators/nodes/IfBlock.ts index cfae03a184..eedb47a614 100644 --- a/src/generators/nodes/IfBlock.ts +++ b/src/generators/nodes/IfBlock.ts @@ -1,16 +1,28 @@ +import deindent from '../../utils/deindent'; import Node from './shared/Node'; +import ElseBlock from './ElseBlock'; import { DomGenerator } from '../dom/index'; import Block from '../dom/Block'; -import { State } from '../dom/interfaces'; +import State from '../dom/State'; import createDebuggingComment from '../../utils/createDebuggingComment'; -function isElseIf(node: Node) { +function isElseIf(node: ElseBlock) { return ( node && node.children.length === 1 && node.children[0].type === 'IfBlock' ); } +function isElseBranch(branch) { + return branch.block && !branch.condition; +} + export default class IfBlock extends Node { + type: 'IfBlock'; + else: ElseBlock; + + _block: Block; + _state: State; + init( block: Block, state: State, @@ -29,7 +41,7 @@ export default class IfBlock extends Node { let hasIntros = false; let hasOutros = false; - function attachBlocks(node: Node) { + function attachBlocks(node: IfBlock) { node.var = block.getUniqueName(`if_block`); block.addDependencies(node.metadata.dependencies); @@ -90,4 +102,412 @@ export default class IfBlock extends Node { generator.blocks.push(...blocks); } + + build( + block: Block, + state: State, + elementStack: Node[], + componentStack: Node[] + ) { + const name = this.var; + + const needsAnchor = this.next ? !this.next.isDomNode() : !state.parentNode || !this.parent.isDomNode(); + const anchor = needsAnchor + ? block.getUniqueName(`${name}_anchor`) + : (this.next && this.next.var) || 'null'; + const params = block.params.join(', '); + + const branches = getBranches(this.generator, block, state, this, elementStack, componentStack); + + const hasElse = isElseBranch(branches[branches.length - 1]); + const if_name = hasElse ? '' : `if (${name}) `; + + const dynamic = branches[0].hasUpdateMethod; // can use [0] as proxy for all, since they necessarily have the same value + const hasOutros = branches[0].hasOutroMethod; + + const vars = { name, needsAnchor, anchor, params, if_name, hasElse }; + + if (this.else) { + if (hasOutros) { + compoundWithOutros( + this.generator, + block, + state, + this, + branches, + dynamic, + vars + ); + } else { + compound(this.generator, block, state, this, branches, dynamic, vars); + } + } else { + simple(this.generator, block, state, this, branches[0], dynamic, vars); + } + + block.builders.create.addLine(`${if_name}${name}.c();`); + + block.builders.claim.addLine( + `${if_name}${name}.l(${state.parentNodes});` + ); + + if (needsAnchor) { + block.addElement( + anchor, + `@createComment()`, + `@createComment()`, + state.parentNode + ); + } + } +} + + + + + +// TODO move all this into the class + +function getBranches( + generator: DomGenerator, + block: Block, + state: State, + node: Node, + elementStack: Node[], + componentStack: Node[] +) { + block.contextualise(node.expression); // TODO remove + + const branches = [ + { + condition: node.metadata.snippet, + block: node._block.name, + hasUpdateMethod: node._block.hasUpdateMethod, + hasIntroMethod: node._block.hasIntroMethod, + hasOutroMethod: node._block.hasOutroMethod, + }, + ]; + + visitChildren(generator, block, state, node, elementStack, componentStack); + + if (isElseIf(node.else)) { + branches.push( + ...getBranches(generator, block, state, node.else.children[0], elementStack, componentStack) + ); + } else { + branches.push({ + condition: null, + block: node.else ? node.else._block.name : null, + hasUpdateMethod: node.else ? node.else._block.hasUpdateMethod : false, + hasIntroMethod: node.else ? node.else._block.hasIntroMethod : false, + hasOutroMethod: node.else ? node.else._block.hasOutroMethod : false, + }); + + if (node.else) { + visitChildren(generator, block, state, node.else, elementStack, componentStack); + } + } + + return branches; +} + +function visitChildren( + generator: DomGenerator, + block: Block, + state: State, + node: Node, + elementStack: Node[], + componentStack: Node[] +) { + node.children.forEach((child: Node) => { + child.build(node._block, node._state, elementStack, componentStack); + }); +} + + + +function simple( + generator: DomGenerator, + block: Block, + state: State, + node: Node, + branch, + dynamic, + { name, needsAnchor, anchor, params, if_name } +) { + block.builders.init.addBlock(deindent` + var ${name} = (${branch.condition}) && ${branch.block}(${params}, #component); + `); + + const mountOrIntro = branch.hasIntroMethod ? 'i' : 'm'; + const targetNode = state.parentNode || '#target'; + const anchorNode = state.parentNode ? 'null' : 'anchor'; + + block.builders.mount.addLine( + `if (${name}) ${name}.${mountOrIntro}(${targetNode}, ${anchorNode});` + ); + + const parentNode = node.parent.isDomNode() ? node.parent.var : `${anchor}.parentNode`; + + const enter = dynamic + ? branch.hasIntroMethod + ? deindent` + if (${name}) { + ${name}.p(changed, ${params}); + } else { + ${name} = ${branch.block}(${params}, #component); + if (${name}) ${name}.c(); + } + + ${name}.i(${parentNode}, ${anchor}); + ` + : deindent` + if (${name}) { + ${name}.p(changed, ${params}); + } else { + ${name} = ${branch.block}(${params}, #component); + ${name}.c(); + ${name}.m(${parentNode}, ${anchor}); + } + ` + : branch.hasIntroMethod + ? deindent` + if (!${name}) { + ${name} = ${branch.block}(${params}, #component); + ${name}.c(); + } + ${name}.i(${parentNode}, ${anchor}); + ` + : deindent` + if (!${name}) { + ${name} = ${branch.block}(${params}, #component); + ${name}.c(); + ${name}.m(${parentNode}, ${anchor}); + } + `; + + // no `p()` here — we don't want to update outroing nodes, + // as that will typically result in glitching + const exit = branch.hasOutroMethod + ? deindent` + ${name}.o(function() { + ${name}.u(); + ${name}.d(); + ${name} = null; + }); + ` + : deindent` + ${name}.u(); + ${name}.d(); + ${name} = null; + `; + + block.builders.update.addBlock(deindent` + if (${branch.condition}) { + ${enter} + } else if (${name}) { + ${exit} + } + `); + + block.builders.unmount.addLine(`${if_name}${name}.u();`); + + block.builders.destroy.addLine(`${if_name}${name}.d();`); +} + +function compound( + generator: DomGenerator, + block: Block, + state: State, + node: Node, + branches, + dynamic, + { name, needsAnchor, anchor, params, hasElse, if_name } +) { + const select_block_type = generator.getUniqueName(`select_block_type`); + const current_block_type = block.getUniqueName(`current_block_type`); + const current_block_type_and = hasElse ? '' : `${current_block_type} && `; + + generator.blocks.push(deindent` + function ${select_block_type}(${params}) { + ${branches + .map(({ condition, block }) => `${condition ? `if (${condition}) ` : ''}return ${block};`) + .join('\n')} + } + `); + + block.builders.init.addBlock(deindent` + var ${current_block_type} = ${select_block_type}(${params}); + var ${name} = ${current_block_type_and}${current_block_type}(${params}, #component); + `); + + const mountOrIntro = branches[0].hasIntroMethod ? 'i' : 'm'; + + const targetNode = state.parentNode || '#target'; + const anchorNode = state.parentNode ? 'null' : 'anchor'; + block.builders.mount.addLine( + `${if_name}${name}.${mountOrIntro}(${targetNode}, ${anchorNode});` + ); + + const parentNode = node.parent.isDomNode() ? node.parent.var : `${anchor}.parentNode`; + + const changeBlock = deindent` + ${hasElse + ? deindent` + ${name}.u(); + ${name}.d(); + ` + : deindent` + if (${name}) { + ${name}.u(); + ${name}.d(); + }`} + ${name} = ${current_block_type_and}${current_block_type}(${params}, #component); + ${if_name}${name}.c(); + ${if_name}${name}.${mountOrIntro}(${parentNode}, ${anchor}); + `; + + if (dynamic) { + block.builders.update.addBlock(deindent` + if (${current_block_type} === (${current_block_type} = ${select_block_type}(${params})) && ${name}) { + ${name}.p(changed, ${params}); + } else { + ${changeBlock} + } + `); + } else { + block.builders.update.addBlock(deindent` + if (${current_block_type} !== (${current_block_type} = ${select_block_type}(${params}))) { + ${changeBlock} + } + `); + } + + block.builders.unmount.addLine(`${if_name}${name}.u();`); + + block.builders.destroy.addLine(`${if_name}${name}.d();`); +} + +// if any of the siblings have outros, we need to keep references to the blocks +// (TODO does this only apply to bidi transitions?) +function compoundWithOutros( + generator: DomGenerator, + block: Block, + state: State, + node: Node, + branches, + dynamic, + { name, needsAnchor, anchor, params, hasElse } +) { + const select_block_type = block.getUniqueName(`select_block_type`); + const current_block_type_index = block.getUniqueName(`current_block_type_index`); + const previous_block_index = block.getUniqueName(`previous_block_index`); + const if_block_creators = block.getUniqueName(`if_block_creators`); + const if_blocks = block.getUniqueName(`if_blocks`); + + const if_current_block_type_index = hasElse + ? '' + : `if (~${current_block_type_index}) `; + + block.addVariable(current_block_type_index); + block.addVariable(name); + + block.builders.init.addBlock(deindent` + var ${if_block_creators} = [ + ${branches.map(branch => branch.block).join(',\n')} + ]; + + var ${if_blocks} = []; + + function ${select_block_type}(${params}) { + ${branches + .map(({ condition, block }, i) => `${condition ? `if (${condition}) ` : ''}return ${block ? i : -1};`) + .join('\n')} + } + `); + + if (hasElse) { + block.builders.init.addBlock(deindent` + ${current_block_type_index} = ${select_block_type}(${params}); + ${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](${params}, #component); + `); + } else { + block.builders.init.addBlock(deindent` + if (~(${current_block_type_index} = ${select_block_type}(${params}))) { + ${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](${params}, #component); + } + `); + } + + const mountOrIntro = branches[0].hasIntroMethod ? 'i' : 'm'; + const targetNode = state.parentNode || '#target'; + const anchorNode = state.parentNode ? 'null' : 'anchor'; + + block.builders.mount.addLine( + `${if_current_block_type_index}${if_blocks}[${current_block_type_index}].${mountOrIntro}(${targetNode}, ${anchorNode});` + ); + + const parentNode = (state.parentNode && !needsAnchor) ? state.parentNode : `${anchor}.parentNode`; + + const destroyOldBlock = deindent` + ${name}.o(function() { + ${if_blocks}[ ${previous_block_index} ].u(); + ${if_blocks}[ ${previous_block_index} ].d(); + ${if_blocks}[ ${previous_block_index} ] = null; + }); + `; + + const createNewBlock = deindent` + ${name} = ${if_blocks}[${current_block_type_index}]; + if (!${name}) { + ${name} = ${if_blocks}[${current_block_type_index}] = ${if_block_creators}[${current_block_type_index}](${params}, #component); + ${name}.c(); + } + ${name}.${mountOrIntro}(${parentNode}, ${anchor}); + `; + + const changeBlock = hasElse + ? deindent` + ${destroyOldBlock} + + ${createNewBlock} + ` + : deindent` + if (${name}) { + ${destroyOldBlock} + } + + if (~${current_block_type_index}) { + ${createNewBlock} + } else { + ${name} = null; + } + `; + + if (dynamic) { + block.builders.update.addBlock(deindent` + var ${previous_block_index} = ${current_block_type_index}; + ${current_block_type_index} = ${select_block_type}(${params}); + if (${current_block_type_index} === ${previous_block_index}) { + ${if_current_block_type_index}${if_blocks}[${current_block_type_index}].p(changed, ${params}); + } else { + ${changeBlock} + } + `); + } else { + block.builders.update.addBlock(deindent` + var ${previous_block_index} = ${current_block_type_index}; + ${current_block_type_index} = ${select_block_type}(${params}); + if (${current_block_type_index} !== ${previous_block_index}) { + ${changeBlock} + } + `); + } + + block.builders.destroy.addLine(deindent` + ${if_current_block_type_index}{ + ${if_blocks}[${current_block_type_index}].u(); + ${if_blocks}[${current_block_type_index}].d(); + } + `); } \ No newline at end of file diff --git a/src/generators/nodes/MustacheTag.ts b/src/generators/nodes/MustacheTag.ts index 59d7723be0..ef0ac1ae62 100644 --- a/src/generators/nodes/MustacheTag.ts +++ b/src/generators/nodes/MustacheTag.ts @@ -1,5 +1,7 @@ import Node from './shared/Node'; import Block from '../dom/Block'; +import State from '../dom/State'; +import visitTag from '../dom/visitors/shared/Tag'; export default class MustacheTag extends Node { init(block: Block) { @@ -7,4 +9,27 @@ export default class MustacheTag extends Node { this.var = block.getUniqueName('text'); block.addDependencies(this.metadata.dependencies); } + + build( + block: Block, + state: State, + elementStack: Node[], + componentStack: Node[] + ) { + const { init } = visitTag( + this.generator, + block, + state, + this, + this.var, + value => `${this.var}.data = ${value};` + ); + + block.addElement( + this.var, + `@createText(${init})`, + `@claimText(${state.parentNodes}, ${init})`, + state.parentNode + ); + } } \ No newline at end of file diff --git a/src/generators/nodes/RawMustacheTag.ts b/src/generators/nodes/RawMustacheTag.ts index a77bc2de2d..94cc21937e 100644 --- a/src/generators/nodes/RawMustacheTag.ts +++ b/src/generators/nodes/RawMustacheTag.ts @@ -1,5 +1,8 @@ +import deindent from '../../utils/deindent'; import Node from './shared/Node'; import Block from '../dom/Block'; +import State from '../dom/State'; +import visitTag from '../dom/visitors/shared/Tag'; export default class RawMustacheTag extends Node { init(block: Block) { @@ -7,4 +10,89 @@ export default class RawMustacheTag extends Node { this.var = block.getUniqueName('raw'); block.addDependencies(this.metadata.dependencies); } + + build( + block: Block, + state: State, + elementStack: Node[], + componentStack: Node[] + ) { + const name = this.var; + + const needsAnchorBefore = this.prev ? this.prev.type !== 'Element' : !state.parentNode; + const needsAnchorAfter = this.next ? this.next.type !== 'Element' : !state.parentNode; + + const anchorBefore = needsAnchorBefore + ? block.getUniqueName(`${name}_before`) + : (this.prev && this.prev.var) || 'null'; + + const anchorAfter = needsAnchorAfter + ? block.getUniqueName(`${name}_after`) + : (this.next && this.next.var) || 'null'; + + let detach: string; + let insert: (content: string) => string; + let useInnerHTML = false; + + if (anchorBefore === 'null' && anchorAfter === 'null') { + useInnerHTML = true; + detach = `${state.parentNode}.innerHTML = '';`; + insert = content => `${state.parentNode}.innerHTML = ${content};`; + } else if (anchorBefore === 'null') { + detach = `@detachBefore(${anchorAfter});`; + insert = content => `${anchorAfter}.insertAdjacentHTML("beforebegin", ${content});`; + } else if (anchorAfter === 'null') { + detach = `@detachAfter(${anchorBefore});`; + insert = content => `${anchorBefore}.insertAdjacentHTML("afterend", ${content});`; + } else { + detach = `@detachBetween(${anchorBefore}, ${anchorAfter});`; + insert = content => `${anchorBefore}.insertAdjacentHTML("afterend", ${content});`; + } + + const { init } = visitTag( + this.generator, + block, + state, + this, + name, + content => deindent` + ${!useInnerHTML && detach} + ${insert(content)} + ` + ); + + // we would have used comments here, but the `insertAdjacentHTML` api only + // exists for `Element`s. + if (needsAnchorBefore) { + block.addElement( + anchorBefore, + `@createElement('noscript')`, + `@createElement('noscript')`, + state.parentNode + ); + } + + function addAnchorAfter() { + block.addElement( + anchorAfter, + `@createElement('noscript')`, + `@createElement('noscript')`, + state.parentNode + ); + } + + if (needsAnchorAfter && anchorBefore === 'null') { + // anchorAfter needs to be in the DOM before we + // insert the HTML... + addAnchorAfter(); + } + + block.builders.mount.addLine(insert(init)); + block.builders.detachRaw.addBlock(detach); + + if (needsAnchorAfter && anchorBefore !== 'null') { + // ...otherwise it should go afterwards + addAnchorAfter(); + } + } } \ No newline at end of file diff --git a/src/generators/nodes/Text.ts b/src/generators/nodes/Text.ts index 1ff2e2c0d8..04eb2d96c8 100644 --- a/src/generators/nodes/Text.ts +++ b/src/generators/nodes/Text.ts @@ -1,3 +1,4 @@ +import { stringify } from '../../utils/stringify'; import Node from './shared/Node'; import Block from '../dom/Block'; import { State } from '../dom/interfaces'; @@ -28,4 +29,20 @@ export default class Text extends Node { this.var = block.getUniqueName(`text`); } + + build( + block: Block, + state: State, + elementStack: Node[], + componentStack: Node[] + ) { + if (this.shouldSkip) return; + + block.addElement( + this.var, + `@createText(${stringify(this.data)})`, + `@claimText(${state.parentNodes}, ${stringify(this.data)})`, + state.parentNode + ); + } } \ No newline at end of file diff --git a/src/generators/nodes/Window.ts b/src/generators/nodes/Window.ts new file mode 100644 index 0000000000..9343347c4e --- /dev/null +++ b/src/generators/nodes/Window.ts @@ -0,0 +1,215 @@ +import deindent from '../../utils/deindent'; +import { stringify } from '../../utils/stringify'; +import flattenReference from '../../utils/flattenReference'; +import isVoidElementName from '../../utils/isVoidElementName'; +import validCalleeObjects from '../../utils/validCalleeObjects'; +import reservedNames from '../../utils/reservedNames'; +import Node from './shared/Node'; +import Block from '../dom/Block'; +import State from '../dom/State'; +import Attribute from './Attribute'; + +const associatedEvents = { + innerWidth: 'resize', + innerHeight: 'resize', + outerWidth: 'resize', + outerHeight: 'resize', + + scrollX: 'scroll', + scrollY: 'scroll', +}; + +const readonly = new Set([ + 'innerWidth', + 'innerHeight', + 'outerWidth', + 'outerHeight', + 'online', +]); + +export default class Window extends Node { + type: 'Window'; + attributes: Attribute[]; + + init( + block: Block, + state: State, + inEachBlock: boolean, + elementStack: Node[], + componentStack: Node[], + stripWhitespace: boolean, + nextSibling: Node + ) { + + } + + build( + block: Block, + state: State, + elementStack: Node[], + componentStack: Node[] + ) { + const { generator } = this; + + const events = {}; + const bindings: Record = {}; + + this.attributes.forEach((attribute: Node) => { + if (attribute.type === 'EventHandler') { + // TODO verify that it's a valid callee (i.e. built-in or declared method) + generator.addSourcemapLocations(attribute.expression); + + let usesState = false; + + attribute.expression.arguments.forEach((arg: Node) => { + block.contextualise(arg, null, true); + const { dependencies } = arg.metadata; + if (dependencies.length) usesState = true; + }); + + const flattened = flattenReference(attribute.expression.callee); + if (flattened.name !== 'event' && flattened.name !== 'this') { + // allow event.stopPropagation(), this.select() etc + generator.code.prependRight( + attribute.expression.start, + `${block.alias('component')}.` + ); + } + + const handlerName = block.getUniqueName(`onwindow${attribute.name}`); + const handlerBody = deindent` + ${usesState && `var state = #component.get();`} + [✂${attribute.expression.start}-${attribute.expression.end}✂]; + `; + + block.builders.init.addBlock(deindent` + function ${handlerName}(event) { + ${handlerBody} + } + window.addEventListener("${attribute.name}", ${handlerName}); + `); + + block.builders.destroy.addBlock(deindent` + window.removeEventListener("${attribute.name}", ${handlerName}); + `); + } + + if (attribute.type === 'Binding') { + // in dev mode, throw if read-only values are written to + if (readonly.has(attribute.name)) { + generator.readonly.add(attribute.value.name); + } + + bindings[attribute.name] = attribute.value.name; + + // bind:online is a special case, we need to listen for two separate events + if (attribute.name === 'online') return; + + const associatedEvent = associatedEvents[attribute.name]; + + if (!events[associatedEvent]) events[associatedEvent] = []; + events[associatedEvent].push( + `${attribute.value.name}: this.${attribute.name}` + ); + + // add initial value + generator.metaBindings.push( + `this._state.${attribute.value.name} = window.${attribute.name};` + ); + } + }); + + const lock = block.getUniqueName(`window_updating`); + + Object.keys(events).forEach(event => { + const handlerName = block.getUniqueName(`onwindow${event}`); + const props = events[event].join(',\n'); + + if (event === 'scroll') { + // TODO other bidirectional bindings... + block.addVariable(lock, 'false'); + } + + const handlerBody = deindent` + ${event === 'scroll' && `${lock} = true;`} + ${generator.options.dev && `component._updatingReadonlyProperty = true;`} + + #component.set({ + ${props} + }); + + ${generator.options.dev && `component._updatingReadonlyProperty = false;`} + ${event === 'scroll' && `${lock} = false;`} + `; + + block.builders.init.addBlock(deindent` + function ${handlerName}(event) { + ${handlerBody} + } + window.addEventListener("${event}", ${handlerName}); + `); + + block.builders.destroy.addBlock(deindent` + window.removeEventListener("${event}", ${handlerName}); + `); + }); + + // special case... might need to abstract this out if we add more special cases + if (bindings.scrollX && bindings.scrollY) { + const observerCallback = block.getUniqueName(`scrollobserver`); + + block.builders.init.addBlock(deindent` + function ${observerCallback}() { + if (${lock}) return; + var x = ${bindings.scrollX + ? `#component.get("${bindings.scrollX}")` + : `window.scrollX`}; + var y = ${bindings.scrollY + ? `#component.get("${bindings.scrollY}")` + : `window.scrollY`}; + window.scrollTo(x, y); + } + `); + + if (bindings.scrollX) + block.builders.init.addLine( + `#component.observe("${bindings.scrollX}", ${observerCallback});` + ); + if (bindings.scrollY) + block.builders.init.addLine( + `#component.observe("${bindings.scrollY}", ${observerCallback});` + ); + } else if (bindings.scrollX || bindings.scrollY) { + const isX = !!bindings.scrollX; + + block.builders.init.addBlock(deindent` + #component.observe("${bindings.scrollX || bindings.scrollY}", function(${isX ? 'x' : 'y'}) { + if (${lock}) return; + window.scrollTo(${isX ? 'x, window.scrollY' : 'window.scrollX, y'}); + }); + `); + } + + // another special case. (I'm starting to think these are all special cases.) + if (bindings.online) { + const handlerName = block.getUniqueName(`onlinestatuschanged`); + block.builders.init.addBlock(deindent` + function ${handlerName}(event) { + #component.set({ ${bindings.online}: navigator.onLine }); + } + window.addEventListener("online", ${handlerName}); + window.addEventListener("offline", ${handlerName}); + `); + + // add initial value + generator.metaBindings.push( + `this._state.${bindings.online} = navigator.onLine;` + ); + + block.builders.destroy.addBlock(deindent` + window.removeEventListener("online", ${handlerName}); + window.removeEventListener("offline", ${handlerName}); + `); + } + } +} diff --git a/src/generators/nodes/index.ts b/src/generators/nodes/index.ts index 787a1667e9..9d73cd5fde 100644 --- a/src/generators/nodes/index.ts +++ b/src/generators/nodes/index.ts @@ -14,6 +14,7 @@ import RawMustacheTag from './RawMustacheTag'; import Slot from './Slot'; import Text from './Text'; import ThenBlock from './ThenBlock'; +import Window from './Window'; const nodes: Record = { AwaitBlock, @@ -30,7 +31,8 @@ const nodes: Record = { RawMustacheTag, Slot, Text, - ThenBlock + ThenBlock, + Window }; export default nodes; \ No newline at end of file diff --git a/src/generators/nodes/shared/Node.ts b/src/generators/nodes/shared/Node.ts index 6d651602c2..384cedffef 100644 --- a/src/generators/nodes/shared/Node.ts +++ b/src/generators/nodes/shared/Node.ts @@ -10,6 +10,8 @@ export default class Node { type: string; parent: Node; + prev?: Node; + next?: Node; generator: DomGenerator; canUseInnerHTML: boolean; @@ -61,7 +63,7 @@ export default class Node { // special case — this is an easy way to remove whitespace surrounding // <:Window/>. lil hacky but it works - if (child.type === 'Element' && child.name === ':Window') { + if (child.type === 'Window') { windowComponent = child; return; } @@ -120,7 +122,6 @@ export default class Node { build( block: Block, state: State, - node: Node, elementStack: Node[], componentStack: Node[] ) { @@ -132,4 +133,8 @@ export default class Node { this.parent.type === 'Component' || this.parent.isChildOfComponent() : false; } + + isDomNode() { + return this.type === 'Element' || this.type === 'Text' || this.type === 'MustacheTag'; + } } \ No newline at end of file diff --git a/src/generators/server-side-rendering/visitors/Element.ts b/src/generators/server-side-rendering/visitors/Element.ts index c02fa6c8d3..96bd4b7706 100644 --- a/src/generators/server-side-rendering/visitors/Element.ts +++ b/src/generators/server-side-rendering/visitors/Element.ts @@ -2,16 +2,11 @@ import visitComponent from './Component'; import visitSlot from './Slot'; import isVoidElementName from '../../../utils/isVoidElementName'; import visit from '../visit'; -import visitWindow from './meta/Window'; import { SsrGenerator } from '../index'; import Block from '../Block'; import { escape } from '../../../utils/stringify'; import { Node } from '../../../interfaces'; -const meta = { - ':Window': visitWindow, -}; - function stringifyAttributeValue(block: Block, chunks: Node[]) { return chunks .map((chunk: Node) => { @@ -31,10 +26,6 @@ export default function visitElement( block: Block, node: Node ) { - if (node.name in meta) { - return meta[node.name](generator, block, node); - } - if (node.name === 'slot') { visitSlot(generator, block, node); return; diff --git a/src/generators/server-side-rendering/visitors/meta/Window.ts b/src/generators/server-side-rendering/visitors/Window.ts similarity index 100% rename from src/generators/server-side-rendering/visitors/meta/Window.ts rename to src/generators/server-side-rendering/visitors/Window.ts diff --git a/src/generators/server-side-rendering/visitors/index.ts b/src/generators/server-side-rendering/visitors/index.ts index 5a8031c0ea..e73ded6793 100644 --- a/src/generators/server-side-rendering/visitors/index.ts +++ b/src/generators/server-side-rendering/visitors/index.ts @@ -7,6 +7,7 @@ import IfBlock from './IfBlock'; import MustacheTag from './MustacheTag'; import RawMustacheTag from './RawMustacheTag'; import Text from './Text'; +import Window from './Window'; export default { AwaitBlock, @@ -17,5 +18,6 @@ export default { IfBlock, MustacheTag, RawMustacheTag, - Text + Text, + Window };