start moving build logic into individual nodes

pull/992/head
Rich Harris 8 years ago
parent 39f1af26ae
commit 8e9e323b13

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

@ -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'

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

@ -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,

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

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

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

@ -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<string, any> = {
':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[] = [];

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

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

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

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

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

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

@ -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<string, string> = {};
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});
`);
}
}
}

@ -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<string, any> = {
AwaitBlock,
@ -30,7 +31,8 @@ const nodes: Record<string, any> = {
RawMustacheTag,
Slot,
Text,
ThenBlock
ThenBlock,
Window
};
export default nodes;

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

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

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

Loading…
Cancel
Save