From e8e05e716066ba9d6efc9138f5c644f502ed73d3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 9 Dec 2017 11:31:04 -0500 Subject: [PATCH] move Slot logic --- src/generators/Generator.ts | 9 +- src/generators/dom/visitors/Slot.ts | 104 ------------ src/generators/nodes/Element.ts | 11 +- src/generators/nodes/Slot.ts | 152 +++++++++++++++++- .../server-side-rendering/visitors/index.ts | 2 + 5 files changed, 162 insertions(+), 116 deletions(-) delete mode 100644 src/generators/dom/visitors/Slot.ts diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index e9b7805dfc..148e659365 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -163,11 +163,9 @@ export default class Generator { this.computations = []; this.templateProperties = {}; + this.name = this.alias(name); this.walkJs(dom); - this.walkTemplate(); - - this.name = this.alias(name); if (options.customElement === true) { this.customElement = { @@ -181,6 +179,8 @@ export default class Generator { if (this.customElement && !this.customElement.tag) { throw new Error(`No tag name specified`); // TODO better error } + + this.walkTemplate(); } addSourcemapLocations(node: Node) { @@ -718,6 +718,9 @@ export default class Generator { } else if (node.name === ':Window') { // TODO do this in parse? node.type = 'Window'; node.__proto__ = nodes.Window.prototype; + } else if (node.type === 'Element' && node.name === 'slot' && !generator.customElement) { + node.type = 'Slot'; + node.__proto__ = nodes.Slot.prototype; } else if (node.type in nodes) { node.__proto__ = nodes[node.type].prototype; } diff --git a/src/generators/dom/visitors/Slot.ts b/src/generators/dom/visitors/Slot.ts deleted file mode 100644 index fe73864d6e..0000000000 --- a/src/generators/dom/visitors/Slot.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { DomGenerator } from '../index'; -import deindent from '../../../utils/deindent'; -import visit from '../visit'; -import Block from '../Block'; -import getStaticAttributeValue from '../../../utils/getStaticAttributeValue'; -import { Node } from '../../../interfaces'; -import { State } from '../interfaces'; - -export default function visitSlot( - generator: DomGenerator, - block: Block, - state: State, - node: Node, - elementStack: Node[], - componentStack: Node[] -) { - const slotName = getStaticAttributeValue(node, 'name') || 'default'; - generator.slots.add(slotName); - - const content_name = block.getUniqueName(`slot_content_${slotName}`); - block.addVariable(content_name, `#component._slotted.${slotName}`); - - const needsAnchorBefore = node.prev ? node.prev.type !== 'Element' : !state.parentNode; - const needsAnchorAfter = node.next ? node.next.type !== 'Element' : !state.parentNode; - - const anchorBefore = needsAnchorBefore - ? block.getUniqueName(`${content_name}_before`) - : (node.prev && node.prev.var) || 'null'; - - const anchorAfter = needsAnchorAfter - ? block.getUniqueName(`${content_name}_after`) - : (node.next && node.next.var) || 'null'; - - if (needsAnchorBefore) block.addVariable(anchorBefore); - if (needsAnchorAfter) block.addVariable(anchorAfter); - - block.builders.create.pushCondition(`!${content_name}`); - block.builders.hydrate.pushCondition(`!${content_name}`); - block.builders.mount.pushCondition(`!${content_name}`); - block.builders.unmount.pushCondition(`!${content_name}`); - block.builders.destroy.pushCondition(`!${content_name}`); - - node.children.forEach((child: Node) => { - child.build(block, state, elementStack, componentStack); - }); - - block.builders.create.popCondition(); - block.builders.hydrate.popCondition(); - block.builders.mount.popCondition(); - block.builders.unmount.popCondition(); - block.builders.destroy.popCondition(); - - // TODO can we use an else here? - if (state.parentNode) { - block.builders.mount.addBlock(deindent` - if (${content_name}) { - ${needsAnchorBefore && `@appendNode(${anchorBefore} || (${anchorBefore} = @createComment()), ${state.parentNode});`} - @appendNode(${content_name}, ${state.parentNode}); - ${needsAnchorAfter && `@appendNode(${anchorAfter} || (${anchorAfter} = @createComment()), ${state.parentNode});`} - } - `); - } else { - block.builders.mount.addBlock(deindent` - if (${content_name}) { - ${needsAnchorBefore && `@insertNode(${anchorBefore} || (${anchorBefore} = @createComment()), #target, anchor);`} - @insertNode(${content_name}, #target, anchor); - ${needsAnchorAfter && `@insertNode(${anchorAfter} || (${anchorAfter} = @createComment()), #target, anchor);`} - } - `); - } - - // if the slot is unmounted, move nodes back into the document fragment, - // so that it can be reinserted later - // TODO so that this can work with public API, component._slotted should - // be all fragments, derived from options.slots. Not === options.slots - // TODO can we use an else here? - if (anchorBefore === 'null' && anchorAfter === 'null') { - block.builders.unmount.addBlock(deindent` - if (${content_name}) { - @reinsertChildren(${state.parentNode}, ${content_name}); - } - `); - } else if (anchorBefore === 'null') { - block.builders.unmount.addBlock(deindent` - if (${content_name}) { - @reinsertBefore(${anchorAfter}, ${content_name}); - } - `); - } else if (anchorAfter === 'null') { - block.builders.unmount.addBlock(deindent` - if (${content_name}) { - @reinsertAfter(${anchorBefore}, ${content_name}); - } - `); - } else { - block.builders.unmount.addBlock(deindent` - if (${content_name}) { - @reinsertBetween(${anchorBefore}, ${anchorAfter}, ${content_name}); - @detachNode(${anchorBefore}); - @detachNode(${anchorAfter}); - } - `); - } -} diff --git a/src/generators/nodes/Element.ts b/src/generators/nodes/Element.ts index 9fa52738f5..7a087626f6 100644 --- a/src/generators/nodes/Element.ts +++ b/src/generators/nodes/Element.ts @@ -15,7 +15,6 @@ import * as namespaces from '../../utils/namespaces'; 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'; @@ -155,13 +154,9 @@ export default class Element extends Node { ) { const { generator } = this; - if (this.name === 'slot') { // TODO deal with in walkTemplate - if (this.generator.customElement) { - const slotName = this.getStaticAttributeValue('name') || 'default'; - this.generator.slots.add(slotName); - } else { - return visitSlot(this.generator, block, state, this, elementStack, componentStack); - } + if (this.name === 'slot') { + const slotName = this.getStaticAttributeValue('name') || 'default'; + this.generator.slots.add(slotName); } const childState = this._state; diff --git a/src/generators/nodes/Slot.ts b/src/generators/nodes/Slot.ts index 07c2ea1070..66c575ba6f 100644 --- a/src/generators/nodes/Slot.ts +++ b/src/generators/nodes/Slot.ts @@ -1,5 +1,155 @@ +import deindent from '../../utils/deindent'; import Node from './shared/Node'; +import Element from './Element'; +import Attribute from './Attribute'; +import Block from '../dom/Block'; +import State from '../dom/State'; -export default class Slot extends Node { +export default class Slot extends Element { + type: 'Element'; + name: string; + attributes: Attribute[]; // TODO have more specific Attribute type + children: Node[]; + init( + block: Block, + state: State, + inEachBlock: boolean, + elementStack: Node[], + componentStack: Node[], + stripWhitespace: boolean, + nextSibling: Node + ) { + this.cannotUseInnerHTML(); + + this.var = block.getUniqueName('slot'); + + this._state = state.child({ + parentNode: this.var, + parentNodes: block.getUniqueName(`${this.var}_nodes`), + parentNodeName: this.name, + namespace: this.name === 'svg' + ? 'http://www.w3.org/2000/svg' + : state.namespace, + allUsedContexts: [], + }); + + if (this.children.length) { + this.initChildren(block, this._state, inEachBlock, elementStack.concat(this), componentStack, stripWhitespace, nextSibling); + } + } + + build( + block: Block, + state: State, + elementStack: Node[], + componentStack: Node[] + ) { + const { generator } = this; + + const slotName = this.getStaticAttributeValue('name') || 'default'; + generator.slots.add(slotName); + + const content_name = block.getUniqueName(`slot_content_${slotName}`); + block.addVariable(content_name, `#component._slotted.${slotName}`); + + 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(`${content_name}_before`) + : (this.prev && this.prev.var) || 'null'; + + const anchorAfter = needsAnchorAfter + ? block.getUniqueName(`${content_name}_after`) + : (this.next && this.next.var) || 'null'; + + if (needsAnchorBefore) block.addVariable(anchorBefore); + if (needsAnchorAfter) block.addVariable(anchorAfter); + + block.builders.create.pushCondition(`!${content_name}`); + block.builders.hydrate.pushCondition(`!${content_name}`); + block.builders.mount.pushCondition(`!${content_name}`); + block.builders.unmount.pushCondition(`!${content_name}`); + block.builders.destroy.pushCondition(`!${content_name}`); + + this.children.forEach((child: Node) => { + child.build(block, state, elementStack, componentStack); + }); + + block.builders.create.popCondition(); + block.builders.hydrate.popCondition(); + block.builders.mount.popCondition(); + block.builders.unmount.popCondition(); + block.builders.destroy.popCondition(); + + // TODO can we use an else here? + if (state.parentNode) { + block.builders.mount.addBlock(deindent` + if (${content_name}) { + ${needsAnchorBefore && `@appendNode(${anchorBefore} || (${anchorBefore} = @createComment()), ${state.parentNode});`} + @appendNode(${content_name}, ${state.parentNode}); + ${needsAnchorAfter && `@appendNode(${anchorAfter} || (${anchorAfter} = @createComment()), ${state.parentNode});`} + } + `); + } else { + block.builders.mount.addBlock(deindent` + if (${content_name}) { + ${needsAnchorBefore && `@insertNode(${anchorBefore} || (${anchorBefore} = @createComment()), #target, anchor);`} + @insertNode(${content_name}, #target, anchor); + ${needsAnchorAfter && `@insertNode(${anchorAfter} || (${anchorAfter} = @createComment()), #target, anchor);`} + } + `); + } + + // if the slot is unmounted, move nodes back into the document fragment, + // so that it can be reinserted later + // TODO so that this can work with public API, component._slotted should + // be all fragments, derived from options.slots. Not === options.slots + // TODO can we use an else here? + if (anchorBefore === 'null' && anchorAfter === 'null') { + block.builders.unmount.addBlock(deindent` + if (${content_name}) { + @reinsertChildren(${state.parentNode}, ${content_name}); + } + `); + } else if (anchorBefore === 'null') { + block.builders.unmount.addBlock(deindent` + if (${content_name}) { + @reinsertBefore(${anchorAfter}, ${content_name}); + } + `); + } else if (anchorAfter === 'null') { + block.builders.unmount.addBlock(deindent` + if (${content_name}) { + @reinsertAfter(${anchorBefore}, ${content_name}); + } + `); + } else { + block.builders.unmount.addBlock(deindent` + if (${content_name}) { + @reinsertBetween(${anchorBefore}, ${anchorAfter}, ${content_name}); + @detachNode(${anchorBefore}); + @detachNode(${anchorAfter}); + } + `); + } + } + + getStaticAttributeValue(name: string) { + const attribute = this.attributes.find( + (attr: Node) => attr.name.toLowerCase() === name + ); + + if (!attribute) return null; + + if (attribute.value === true) return true; + if (attribute.value.length === 0) return ''; + + if (attribute.value.length === 1 && attribute.value[0].type === 'Text') { + return attribute.value[0].data; + } + + return null; + } } \ No newline at end of file diff --git a/src/generators/server-side-rendering/visitors/index.ts b/src/generators/server-side-rendering/visitors/index.ts index e73ded6793..73d51043b5 100644 --- a/src/generators/server-side-rendering/visitors/index.ts +++ b/src/generators/server-side-rendering/visitors/index.ts @@ -6,6 +6,7 @@ import Element from './Element'; import IfBlock from './IfBlock'; import MustacheTag from './MustacheTag'; import RawMustacheTag from './RawMustacheTag'; +import Slot from './Slot'; import Text from './Text'; import Window from './Window'; @@ -18,6 +19,7 @@ export default { IfBlock, MustacheTag, RawMustacheTag, + Slot, Text, Window };