diff --git a/src/generators/Generator.ts b/src/generators/Generator.ts index b94f74e488..ef21649a47 100644 --- a/src/generators/Generator.ts +++ b/src/generators/Generator.ts @@ -43,6 +43,7 @@ export default class Generator { hasJs: boolean; computations: Computation[]; templateProperties: Record; + slots: Set; code: MagicString; @@ -76,6 +77,7 @@ export default class Generator { this.events = new Set(); this.transitions = new Set(); this.importedComponents = new Map(); + this.slots = new Set(); this.bindingGroups = []; this.indirectDependencies = new Map(); diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts index 4e20b28e3d..f606e61d8c 100644 --- a/src/generators/dom/index.ts +++ b/src/generators/dom/index.ts @@ -72,7 +72,7 @@ export default function dom( generator.stylesheet.warnOnUnusedSelectors(options.onwarn); parsed.html.children.forEach((node: Node) => { - visit(generator, block, state, node, []); + visit(generator, block, state, node, [], []); }); const builder = new CodeBuilder(); @@ -181,6 +181,7 @@ export default function dom( this._root = options._root || this; this._yield = options._yield; this._bind = options._bind; + ${generator.slots.size && `this._slotted = options.slots || {};`} ${generator.stylesheet.hasStyles && options.css !== false && @@ -200,6 +201,8 @@ export default function dom( `} `} + ${generator.slots.size && `this.slots = {};`} + this._fragment = @create_main_fragment( this._state, this ); if ( options.target ) { diff --git a/src/generators/dom/interfaces.ts b/src/generators/dom/interfaces.ts index 858680b7df..37dc449d1d 100644 --- a/src/generators/dom/interfaces.ts +++ b/src/generators/dom/interfaces.ts @@ -1,3 +1,7 @@ +import { DomGenerator } from './index'; +import Block from './Block'; +import { Node } from '../../interfaces'; + export interface State { name?: string; namespace: string; @@ -11,3 +15,12 @@ export interface State { usesComponent?: boolean; selectBindingDependencies?: string[]; } + +export type Visitor = ( + generator: DomGenerator, + block: Block, + state: State, + node: Node, + elementStack: Node[], + componentStack: Node[] +) => void; \ No newline at end of file diff --git a/src/generators/dom/preprocess.ts b/src/generators/dom/preprocess.ts index 9b784bc558..6beca924ff 100644 --- a/src/generators/dom/preprocess.ts +++ b/src/generators/dom/preprocess.ts @@ -1,6 +1,7 @@ import Block from './Block'; import { trimStart, trimEnd } from '../../utils/trim'; import { assign } from '../../shared/index.js'; +import getStaticAttributeValue from '../shared/getStaticAttributeValue'; import { DomGenerator } from './index'; import { Node } from '../../interfaces'; import { State } from './interfaces'; @@ -41,6 +42,7 @@ const preprocessors = { state: State, node: Node, elementStack: Node[], + componentStack: Node[], stripWhitespace: boolean ) => { const dependencies = block.findDependencies(node.expression); @@ -57,6 +59,7 @@ const preprocessors = { state: State, node: Node, elementStack: Node[], + componentStack: Node[], stripWhitespace: boolean ) => { const dependencies = block.findDependencies(node.expression); @@ -74,6 +77,7 @@ const preprocessors = { state: State, node: Node, elementStack: Node[], + componentStack: Node[], stripWhitespace: boolean ) => { node._state = getChildState(state); @@ -94,6 +98,7 @@ const preprocessors = { node: Node, inEachBlock: boolean, elementStack: Node[], + componentStack: Node[], stripWhitespace: boolean, nextSibling: Node ) => { @@ -113,7 +118,7 @@ const preprocessors = { node._state = getChildState(state); blocks.push(node._block); - preprocessChildren(generator, node._block, node._state, node, inEachBlock, elementStack, stripWhitespace, nextSibling); + preprocessChildren(generator, node._block, node._state, node, inEachBlock, elementStack, componentStack, stripWhitespace, nextSibling); if (node._block.dependencies.size > 0) { dynamic = true; @@ -140,6 +145,7 @@ const preprocessors = { node.else, inEachBlock, elementStack, + componentStack, stripWhitespace, nextSibling ); @@ -169,6 +175,7 @@ const preprocessors = { node: Node, inEachBlock: boolean, elementStack: Node[], + componentStack: Node[], stripWhitespace: boolean, nextSibling: Node ) => { @@ -221,7 +228,7 @@ const preprocessors = { }); generator.blocks.push(node._block); - preprocessChildren(generator, node._block, node._state, node, true, elementStack, stripWhitespace, nextSibling); + preprocessChildren(generator, node._block, node._state, node, true, elementStack, componentStack, stripWhitespace, nextSibling); block.addDependencies(node._block.dependencies); node._block.hasUpdateMethod = node._block.dependencies.size > 0; @@ -240,6 +247,7 @@ const preprocessors = { node.else, inEachBlock, elementStack, + componentStack, stripWhitespace, nextSibling ); @@ -254,6 +262,7 @@ const preprocessors = { node: Node, inEachBlock: boolean, elementStack: Node[], + componentStack: Node[], stripWhitespace: boolean, nextSibling: Node ) => { @@ -326,10 +335,23 @@ const preprocessors = { generator.components.has(node.name) || node.name === ':Self'; if (isComponent) { + const name = block.getUniqueName( + (node.name === ':Self' ? generator.name : node.name).toLowerCase() + ); + node._state = getChildState(state, { + name, + parentNode: `${name}._slotted.default`, isYield: true }); } else { + const slot = getStaticAttributeValue(node, 'slot'); + if (slot) { + // TODO validate slots — no nesting, no dynamic names... + const component = componentStack[componentStack.length - 1]; + component._slots.add(slot); + } + const name = block.getUniqueName( node.name.replace(/[^a-zA-Z0-9_$]/g, '_') ); @@ -355,17 +377,12 @@ const preprocessors = { (node.name === ':Self' ? generator.name : node.name).toLowerCase() ); - node._block = block.child({ - name: generator.getUniqueName(`create_${name}_yield_fragment`), - }); + if (node.children) node._slots = new Set(['default']); // TODO only include default if there are unslotted children - generator.blocks.push(node._block); - preprocessChildren(generator, node._block, node._state, node, inEachBlock, elementStack, stripWhitespace, nextSibling); - block.addDependencies(node._block.dependencies); - node._block.hasUpdateMethod = node._block.dependencies.size > 0; + preprocessChildren(generator, block, node._state, node, inEachBlock, elementStack, componentStack.concat(node), stripWhitespace, nextSibling); } else { if (node.name === 'pre' || node.name === 'textarea') stripWhitespace = false; - preprocessChildren(generator, block, node._state, node, inEachBlock, elementStack.concat(node), stripWhitespace, nextSibling); + preprocessChildren(generator, block, node._state, node, inEachBlock, elementStack.concat(node), componentStack, stripWhitespace, nextSibling); } } }, @@ -378,6 +395,7 @@ function preprocessChildren( node: Node, inEachBlock: boolean, elementStack: Node[], + componentStack: Node[], stripWhitespace: boolean, nextSibling: Node ) { @@ -407,7 +425,7 @@ function preprocessChildren( cleaned.forEach((child: Node, i: number) => { const preprocessor = preprocessors[child.type]; - if (preprocessor) preprocessor(generator, block, state, child, inEachBlock, elementStack, stripWhitespace, cleaned[i + 1] || nextSibling); + if (preprocessor) preprocessor(generator, block, state, child, inEachBlock, elementStack, componentStack, stripWhitespace, cleaned[i + 1] || nextSibling); if (lastChild) { lastChild.next = child; @@ -471,7 +489,7 @@ export default function preprocess( }; generator.blocks.push(block); - preprocessChildren(generator, block, state, node, false, [], true, null); + preprocessChildren(generator, block, state, node, false, [], [], true, null); block.hasUpdateMethod = true; return { block, state }; diff --git a/src/generators/dom/visit.ts b/src/generators/dom/visit.ts index 82fc23c03b..c5e8f0bed4 100644 --- a/src/generators/dom/visit.ts +++ b/src/generators/dom/visit.ts @@ -9,8 +9,9 @@ export default function visit( block: Block, state: State, node: Node, - elementStack: Node[] + elementStack: Node[], + componentStack: Node[] ) { const visitor = visitors[node.type]; - visitor(generator, block, state, node, elementStack); + visitor(generator, block, state, node, elementStack, componentStack); } diff --git a/src/generators/dom/visitors/Component.ts b/src/generators/dom/visitors/Component.ts index e474eefd60..6757de1a8a 100644 --- a/src/generators/dom/visitors/Component.ts +++ b/src/generators/dom/visitors/Component.ts @@ -43,47 +43,22 @@ export default function visitComponent( block: Block, state: State, node: Node, - elementStack: Node[] + elementStack: Node[], + componentStack: Node[] ) { generator.hasComponents = true; - const name = block.getUniqueName( - (node.name === ':Self' ? generator.name : node.name).toLowerCase() - ); + const name = node._state.name; const componentInitProperties = [`_root: #component._root`]; - // Component has children, put them in a separate {{yield}} block if (node.children.length > 0) { - const params = block.params.join(', '); - - const childBlock = node._block; + const slots = Array.from(node._slots).map(name => `${name}: @createFragment()`); + componentInitProperties.push(`slots: { ${slots.join(', ')} }`); node.children.forEach((child: Node) => { - visit(generator, childBlock, node._state, child, elementStack); + visit(generator, block, node._state, child, elementStack, componentStack.concat(node)); }); - - const yield_fragment = block.getUniqueName(`${name}_yield_fragment`); - - block.builders.init.addLine( - `var ${yield_fragment} = ${childBlock.name}( ${params}, #component );` - ); - - block.builders.create.addLine(`${yield_fragment}.create();`); - - block.builders.claim.addLine( - `${yield_fragment}.claim( ${state.parentNodes} );` - ); - - if (childBlock.hasUpdateMethod) { - block.builders.update.addLine( - `${yield_fragment}.update( changed, ${params} );` - ); - } - - block.builders.destroy.addLine(`${yield_fragment}.destroy();`); - - componentInitProperties.push(`_yield: ${yield_fragment}`); } const allContexts = new Set(); diff --git a/src/generators/dom/visitors/EachBlock.ts b/src/generators/dom/visitors/EachBlock.ts index 1c8a9c9346..ee4796946d 100644 --- a/src/generators/dom/visitors/EachBlock.ts +++ b/src/generators/dom/visitors/EachBlock.ts @@ -10,7 +10,8 @@ export default function visitEachBlock( block: Block, state: State, node: Node, - elementStack: Node[] + elementStack: Node[], + componentStack: Node[] ) { const each_block = generator.getUniqueName(`each_block`); const create_each_block = node._block.name; @@ -125,12 +126,12 @@ export default function visitEachBlock( } node.children.forEach((child: Node) => { - visit(generator, node._block, node._state, child, elementStack); + visit(generator, node._block, node._state, child, elementStack, componentStack); }); if (node.else) { node.else.children.forEach((child: Node) => { - visit(generator, node.else._block, node.else._state, child, elementStack); + visit(generator, node.else._block, node.else._state, child, elementStack, componentStack); }); } } diff --git a/src/generators/dom/visitors/Element/Attribute.ts b/src/generators/dom/visitors/Element/Attribute.ts index b743971220..042c1abdce 100644 --- a/src/generators/dom/visitors/Element/Attribute.ts +++ b/src/generators/dom/visitors/Element/Attribute.ts @@ -1,7 +1,7 @@ import attributeLookup from './lookup'; import deindent from '../../../../utils/deindent'; import { stringify } from '../../../../utils/stringify'; -import getStaticAttributeValue from './getStaticAttributeValue'; +import getStaticAttributeValue from '../../../shared/getStaticAttributeValue'; import { DomGenerator } from '../../index'; import Block from '../../Block'; import { Node } from '../../../../interfaces'; diff --git a/src/generators/dom/visitors/Element/Binding.ts b/src/generators/dom/visitors/Element/Binding.ts index 77801c7876..a99b028192 100644 --- a/src/generators/dom/visitors/Element/Binding.ts +++ b/src/generators/dom/visitors/Element/Binding.ts @@ -1,6 +1,6 @@ import deindent from '../../../../utils/deindent'; import flattenReference from '../../../../utils/flattenReference'; -import getStaticAttributeValue from './getStaticAttributeValue'; +import getStaticAttributeValue from '../../../shared/getStaticAttributeValue'; import { DomGenerator } from '../../index'; import Block from '../../Block'; import { Node } from '../../../../interfaces'; diff --git a/src/generators/dom/visitors/Element/Element.ts b/src/generators/dom/visitors/Element/Element.ts index 263cfbf2b3..f0e97d9f15 100644 --- a/src/generators/dom/visitors/Element/Element.ts +++ b/src/generators/dom/visitors/Element/Element.ts @@ -1,5 +1,6 @@ import deindent from '../../../../utils/deindent'; import visit from '../../visit'; +import visitSlot from '../Slot'; import visitComponent from '../Component'; import visitWindow from './meta/Window'; import visitAttribute from './Attribute'; @@ -36,20 +37,30 @@ export default function visitElement( block: Block, state: State, node: Node, - elementStack: Node[] + elementStack: Node[], + componentStack: Node[] ) { if (node.name in meta) { return meta[node.name](generator, block, node); } + if (node.name === 'slot') { + return visitSlot(generator, block, state, node, elementStack); + } + if (generator.components.has(node.name) || node.name === ':Self') { - return visitComponent(generator, block, state, node, elementStack); + return visitComponent(generator, block, state, node, elementStack, componentStack); } const childState = node._state; const name = childState.parentNode; - const isToplevel = !state.parentNode; + const slot = node.attributes.find((attribute: Node) => attribute.name === 'slot'); + const parentNode = slot ? + `${componentStack[componentStack.length - 1]._state.name}._slotted.${slot.value[0].data}` : // TODO this looks bonkers + state.parentNode; + + const isToplevel = !parentNode; block.addVariable(name); block.builders.create.addLine( @@ -62,19 +73,14 @@ export default function visitElement( if (generator.hydratable) { block.builders.claim.addBlock(deindent` - ${name} = ${getClaimStatement( - generator, - childState.namespace, - state.parentNodes, - node - )}; + ${name} = ${getClaimStatement(generator, childState.namespace, state.parentNodes, node)}; var ${childState.parentNodes} = @children( ${name} ); `); } - if (state.parentNode) { + if (parentNode) { block.builders.mount.addLine( - `@appendNode( ${name}, ${state.parentNode} );` + `@appendNode( ${name}, ${parentNode} );` ); } else { block.builders.mount.addLine(`@insertNode( ${name}, #target, anchor );`); @@ -190,7 +196,7 @@ export default function visitElement( } node.children.forEach((child: Node) => { - visit(generator, block, childState, child, elementStack.concat(node)); + visit(generator, block, childState, child, elementStack.concat(node), componentStack); }); if (node.lateUpdate) { diff --git a/src/generators/dom/visitors/IfBlock.ts b/src/generators/dom/visitors/IfBlock.ts index 193edd872b..e45ab1fb20 100644 --- a/src/generators/dom/visitors/IfBlock.ts +++ b/src/generators/dom/visitors/IfBlock.ts @@ -20,7 +20,8 @@ function getBranches( block: Block, state: State, node: Node, - elementStack: Node[] + elementStack: Node[], + componentStack: Node[] ) { const branches = [ { @@ -32,11 +33,11 @@ function getBranches( }, ]; - visitChildren(generator, block, state, node, elementStack); + visitChildren(generator, block, state, node, elementStack, componentStack); if (isElseIf(node.else)) { branches.push( - ...getBranches(generator, block, state, node.else.children[0], elementStack) + ...getBranches(generator, block, state, node.else.children[0], elementStack, componentStack) ); } else { branches.push({ @@ -48,7 +49,7 @@ function getBranches( }); if (node.else) { - visitChildren(generator, block, state, node.else, elementStack); + visitChildren(generator, block, state, node.else, elementStack, componentStack); } } @@ -60,10 +61,11 @@ function visitChildren( block: Block, state: State, node: Node, - elementStack: Node[] + elementStack: Node[], + componentStack: Node[] ) { node.children.forEach((child: Node) => { - visit(generator, node._block, node._state, child, elementStack); + visit(generator, node._block, node._state, child, elementStack, componentStack); }); } @@ -72,7 +74,8 @@ export default function visitIfBlock( block: Block, state: State, node: Node, - elementStack: Node[] + elementStack: Node[], + componentStack: Node[] ) { const name = generator.getUniqueName(`if_block`); const anchor = node.needsAnchor @@ -80,7 +83,7 @@ export default function visitIfBlock( : (node.next && node.next._state.name) || 'null'; const params = block.params.join(', '); - const branches = getBranches(generator, block, state, node, elementStack); + const branches = getBranches(generator, block, state, node, elementStack, componentStack); const hasElse = isElseBranch(branches[branches.length - 1]); const if_name = hasElse ? '' : `if ( ${name} ) `; diff --git a/src/generators/dom/visitors/Slot.ts b/src/generators/dom/visitors/Slot.ts new file mode 100644 index 0000000000..85f53f2ca3 --- /dev/null +++ b/src/generators/dom/visitors/Slot.ts @@ -0,0 +1,76 @@ +import { DomGenerator } from '../index'; +import deindent from '../../../utils/deindent'; +import visit from '../visit'; +import Block from '../Block'; +import getStaticAttributeValue from '../../shared/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}`); + + // TODO use surrounds as anchors where possible, a la if/each blocks + const before = block.getUniqueName(`${content_name}_before`); + const after = block.getUniqueName(`${content_name}_after`); + block.addVariable(before); + block.addVariable(after); + + 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) => { + visit(generator, block, state, child, 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}) { + @appendNode(${before} || (${before} = @createComment()), ${state.parentNode}); + @appendNode(${content_name}, ${state.parentNode}); + @appendNode(${after} || (${after} = @createComment()), ${state.parentNode}); + } + `); + } else { + block.builders.mount.addBlock(deindent` + if (${content_name}) { + @insertNode(${before} || (${before} = @createComment()), #target, anchor); + @insertNode(${content_name}, #target, anchor); + @insertNode(${after} || (${after} = @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? + block.builders.unmount.addBlock(deindent` + if (${content_name}) { + @reinsertBetween(${before}, ${after}, ${content_name}); + @detachNode(${before}); + @detachNode(${after}); + } + `); +} diff --git a/src/generators/dom/visitors/index.ts b/src/generators/dom/visitors/index.ts index c690a2ba9c..3a6a1d02cf 100644 --- a/src/generators/dom/visitors/index.ts +++ b/src/generators/dom/visitors/index.ts @@ -5,8 +5,9 @@ import MustacheTag from './MustacheTag'; import RawMustacheTag from './RawMustacheTag'; import Text from './Text'; import YieldTag from './YieldTag'; +import { Visitor } from '../interfaces'; -export default { +const visitors: Record = { EachBlock, Element, IfBlock, @@ -15,3 +16,5 @@ export default { Text, YieldTag, }; + +export default visitors; \ No newline at end of file diff --git a/src/generators/server-side-rendering/index.ts b/src/generators/server-side-rendering/index.ts index 0f1771da7a..9419bde3d5 100644 --- a/src/generators/server-side-rendering/index.ts +++ b/src/generators/server-side-rendering/index.ts @@ -11,7 +11,9 @@ import { stringify } from '../../utils/stringify'; export class SsrGenerator extends Generator { bindings: string[]; renderCode: string; - elementDepth: number; + elementDepth: number; // TODO is this necessary? appears to be unused + appendTargets: Record | null; + appendTarget: string | null; constructor( parsed: Parsed, @@ -24,6 +26,7 @@ export class SsrGenerator extends Generator { this.bindings = []; this.renderCode = ''; this.elementDepth = 0; + this.appendTargets = null; // in an SSR context, we don't need to include events, methods, oncreate or ondestroy const { templateProperties, defaultExport } = this; @@ -59,7 +62,23 @@ export class SsrGenerator extends Generator { } append(code: string) { - this.renderCode += code; + if (this.appendTarget) { + this.appendTargets[this.appendTarget] += code; + } else { + this.renderCode += code; + } + } + + removeAppendTarget() { + this.appendTarget = this.appendTargets = null; + } + + setAppendTarget(name: string) { + if (!this.appendTargets[name]) { + this.appendTargets[name] = ''; + } + + this.appendTarget = name; } } diff --git a/src/generators/server-side-rendering/visitors/Component.ts b/src/generators/server-side-rendering/visitors/Component.ts index 0b04a13b11..eb3dbf4f14 100644 --- a/src/generators/server-side-rendering/visitors/Component.ts +++ b/src/generators/server-side-rendering/visitors/Component.ts @@ -80,19 +80,25 @@ export default function visitComponent( let open = `\${${expression}.render({${props}}`; if (node.children.length) { - open += `, { yield: () => \``; - } + generator.appendTargets = {}; + generator.setAppendTarget('default'); - generator.append(open); + generator.elementDepth += 1; - generator.elementDepth += 1; + node.children.forEach((child: Node) => { + visit(generator, block, child); + }); - node.children.forEach((child: Node) => { - visit(generator, block, child); - }); + generator.elementDepth -= 1; - generator.elementDepth -= 1; + const slotted = Object.keys(generator.appendTargets) + .map(name => `${name}: () => \`${generator.appendTargets[name]}\``) + .join(', '); - const close = node.children.length ? `\` })}` : ')}'; - generator.append(close); + open += `, { slotted: { ${slotted} } }`; + generator.setAppendTarget(null); + } + + generator.append(open); + generator.append(')}'); } diff --git a/src/generators/server-side-rendering/visitors/Element.ts b/src/generators/server-side-rendering/visitors/Element.ts index 6204366163..7d2ba15030 100644 --- a/src/generators/server-side-rendering/visitors/Element.ts +++ b/src/generators/server-side-rendering/visitors/Element.ts @@ -1,4 +1,5 @@ import visitComponent from './Component'; +import visitSlot from './Slot'; import isVoidElementName from '../../../utils/isVoidElementName'; import visit from '../visit'; import visitWindow from './meta/Window'; @@ -33,6 +34,11 @@ export default function visitElement( return meta[node.name](generator, block, node); } + if (node.name === 'slot') { + visitSlot(generator, block, node); + return; + } + if (generator.components.has(node.name) || node.name === ':Self') { visitComponent(generator, block, node); return; @@ -41,6 +47,11 @@ export default function visitElement( let openingTag = `<${node.name}`; let textareaContents; // awkward special case + const slot = node.attributes.find((attribute: Node) => attribute.name === 'slot'); + if (slot) { + generator.setAppendTarget(slot.value[0].data); + } + node.attributes.forEach((attribute: Node) => { if (attribute.type !== 'Attribute') return; diff --git a/src/generators/server-side-rendering/visitors/Slot.ts b/src/generators/server-side-rendering/visitors/Slot.ts new file mode 100644 index 0000000000..3886f8a4d9 --- /dev/null +++ b/src/generators/server-side-rendering/visitors/Slot.ts @@ -0,0 +1,26 @@ +import visit from '../visit'; +import { SsrGenerator } from '../index'; +import Block from '../Block'; +import { Node } from '../../../interfaces'; + +export default function visitSlot( + generator: SsrGenerator, + block: Block, + node: Node +) { + // TODO named slots + const name = node.attributes.find((attribute: Node) => attribute.name); + const slotName = name && name.value[0].data || 'default'; + + generator.append(`\${options && options.slotted && options.slotted.${slotName} ? options.slotted.${slotName}() : '`); + + generator.elementDepth += 1; + + node.children.forEach((child: Node) => { + visit(generator, block, child); + }); + + generator.elementDepth -= 1; + + generator.append(`'}`); +} diff --git a/src/generators/server-side-rendering/visitors/YieldTag.ts b/src/generators/server-side-rendering/visitors/YieldTag.ts deleted file mode 100644 index 03c83655ab..0000000000 --- a/src/generators/server-side-rendering/visitors/YieldTag.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { SsrGenerator } from '../index'; - -export default function visitYieldTag(generator: SsrGenerator) { - generator.append(`\${options && options.yield ? options.yield() : ''}`); -} diff --git a/src/generators/server-side-rendering/visitors/index.ts b/src/generators/server-side-rendering/visitors/index.ts index 57dc4a7571..0ed4a58e22 100644 --- a/src/generators/server-side-rendering/visitors/index.ts +++ b/src/generators/server-side-rendering/visitors/index.ts @@ -5,7 +5,6 @@ import IfBlock from './IfBlock'; import MustacheTag from './MustacheTag'; import RawMustacheTag from './RawMustacheTag'; import Text from './Text'; -import YieldTag from './YieldTag'; export default { Comment, @@ -14,6 +13,5 @@ export default { IfBlock, MustacheTag, RawMustacheTag, - Text, - YieldTag, + Text }; diff --git a/src/generators/dom/visitors/Element/getStaticAttributeValue.ts b/src/generators/shared/getStaticAttributeValue.ts similarity index 100% rename from src/generators/dom/visitors/Element/getStaticAttributeValue.ts rename to src/generators/shared/getStaticAttributeValue.ts diff --git a/src/parse/read/directives.ts b/src/parse/read/directives.ts index c0c46d6af9..2a27f37812 100644 --- a/src/parse/read/directives.ts +++ b/src/parse/read/directives.ts @@ -1,5 +1,5 @@ import { parseExpressionAt } from 'acorn'; -import spaces from '../../utils/spaces'; +import repeat from '../../utils/repeat'; import { Parser } from '../index'; function readExpression(parser: Parser, start: number, quoteMark: string|null) { @@ -31,7 +31,7 @@ function readExpression(parser: Parser, start: number, quoteMark: string|null) { } } - const expression = parseExpressionAt(spaces(start) + str, start); + const expression = parseExpressionAt(repeat(' ', start) + str, start); parser.index = expression.end; parser.allowWhitespace(); @@ -101,7 +101,7 @@ export function readBindingDirective( b = parser.index; } - const source = spaces(a) + parser.template.slice(a, b); + const source = repeat(' ', a) + parser.template.slice(a, b); value = parseExpressionAt(source, a); if (value.type !== 'Identifier' && value.type !== 'MemberExpression') { diff --git a/src/parse/read/script.ts b/src/parse/read/script.ts index 42e8729e06..232c32319b 100644 --- a/src/parse/read/script.ts +++ b/src/parse/read/script.ts @@ -1,5 +1,5 @@ import { parse } from 'acorn'; -import spaces from '../../utils/spaces'; +import repeat from '../../utils/repeat'; import { Parser } from '../index'; import { Node } from '../../interfaces'; @@ -12,7 +12,7 @@ export default function readScript(parser: Parser, start: number, attributes: No if (scriptEnd === -1) parser.error(` diff --git a/test/runtime/samples/component-slot-fallback/Nested.html b/test/runtime/samples/component-slot-fallback/Nested.html new file mode 100644 index 0000000000..dcaf46e042 --- /dev/null +++ b/test/runtime/samples/component-slot-fallback/Nested.html @@ -0,0 +1,5 @@ +
+

default fallback content

+

bar fallback content

+

foo fallback content

+
\ No newline at end of file diff --git a/test/runtime/samples/component-slot-fallback/_config.js b/test/runtime/samples/component-slot-fallback/_config.js new file mode 100644 index 0000000000..64f770b5a4 --- /dev/null +++ b/test/runtime/samples/component-slot-fallback/_config.js @@ -0,0 +1,9 @@ +export default { + html: ` +
+

not fallback

+

bar fallback content

+

foo fallback content

+
+ ` +}; diff --git a/test/runtime/samples/component-slot-fallback/main.html b/test/runtime/samples/component-slot-fallback/main.html new file mode 100644 index 0000000000..8894e86940 --- /dev/null +++ b/test/runtime/samples/component-slot-fallback/main.html @@ -0,0 +1,13 @@ + +

not fallback

+
+ + diff --git a/test/runtime/samples/component-slot-named/Nested.html b/test/runtime/samples/component-slot-named/Nested.html new file mode 100644 index 0000000000..665555820a --- /dev/null +++ b/test/runtime/samples/component-slot-named/Nested.html @@ -0,0 +1,5 @@ +
+ + + +
\ No newline at end of file diff --git a/test/runtime/samples/component-slot-named/_config.js b/test/runtime/samples/component-slot-named/_config.js new file mode 100644 index 0000000000..330e219242 --- /dev/null +++ b/test/runtime/samples/component-slot-named/_config.js @@ -0,0 +1,9 @@ +export default { + html: ` +
+ Hello +

bar

+

foo

+
+ ` +}; diff --git a/test/runtime/samples/component-slot-named/main.html b/test/runtime/samples/component-slot-named/main.html new file mode 100644 index 0000000000..76ba5b072b --- /dev/null +++ b/test/runtime/samples/component-slot-named/main.html @@ -0,0 +1,16 @@ + + Hello + +

foo

+

bar

+
+ + diff --git a/test/runtime/samples/component-yield-if/Widget.html b/test/runtime/samples/component-yield-if/Widget.html index a2c8d49daa..31dba7d071 100644 --- a/test/runtime/samples/component-yield-if/Widget.html +++ b/test/runtime/samples/component-yield-if/Widget.html @@ -9,7 +9,7 @@ data () { return { show: false - } + }; } }; diff --git a/test/runtime/samples/component-yield-if/main.html b/test/runtime/samples/component-yield-if/main.html index 61f0909984..c47e6de3c5 100644 --- a/test/runtime/samples/component-yield-if/main.html +++ b/test/runtime/samples/component-yield-if/main.html @@ -1,14 +1,17 @@
{{data}}
+ diff --git a/test/runtime/samples/component-yield-multiple-in-each/_config.js b/test/runtime/samples/component-yield-multiple-in-each/_config.js index 92c29fdf71..3669b3f90b 100644 --- a/test/runtime/samples/component-yield-multiple-in-each/_config.js +++ b/test/runtime/samples/component-yield-multiple-in-each/_config.js @@ -1,10 +1,19 @@ export default { - html: '

Hello Alice

Hello Bob

Hello Charles

', + html: ` +

Hello Alice

+

Hello Bob

+

Hello Charles

+ `, test ( assert, component, target ) { component.set({ people: [ 'Alice', 'Charles', 'Bob' ] }); - assert.htmlEqual( target.innerHTML, `

Hello Alice

Hello Charles

Hello Bob

` ); + + assert.htmlEqual( target.innerHTML, ` +

Hello Alice

+

Hello Charles

+

Hello Bob

+ `); } }; diff --git a/test/runtime/samples/component-yield/_config.js b/test/runtime/samples/component-yield/_config.js index 174d01f1a5..86cb924c9b 100644 --- a/test/runtime/samples/component-yield/_config.js +++ b/test/runtime/samples/component-yield/_config.js @@ -1,3 +1,6 @@ export default { - html: '

Hello

' + html: ` +

Hello + +

` }; diff --git a/test/runtime/samples/flush-before-bindings/_config.js b/test/runtime/samples/flush-before-bindings/_config.js index 07bad1e2c8..1f731d6a34 100644 --- a/test/runtime/samples/flush-before-bindings/_config.js +++ b/test/runtime/samples/flush-before-bindings/_config.js @@ -1,4 +1,4 @@ -import counter from './counter.js'; +// import counter from './counter.js'; export default { 'skip-ssr': true, diff --git a/test/server-side-rendering/samples/component-yield/_expected.html b/test/server-side-rendering/samples/component-yield/_expected.html index fb13e0ab30..49139417a2 100644 --- a/test/server-side-rendering/samples/component-yield/_expected.html +++ b/test/server-side-rendering/samples/component-yield/_expected.html @@ -1 +1,3 @@ -

Hello

+
+

Hello

+
\ No newline at end of file diff --git a/test/validator/index.js b/test/validator/index.js index 9b4f4900b7..176d060faf 100644 --- a/test/validator/index.js +++ b/test/validator/index.js @@ -8,12 +8,13 @@ describe("validate", () => { // add .solo to a sample directory name to only run that test const solo = /\.solo/.test(dir); + const skip = /\.skip/.test(dir); if (solo && process.env.CI) { throw new Error("Forgot to remove `solo: true` from test"); } - (solo ? it.only : it)(dir, () => { + (solo ? it.only : skip ? it.skip : it)(dir, () => { const filename = `test/validator/samples/${dir}/input.html`; const input = fs.readFileSync(filename, "utf-8").replace(/\s+$/, ""); diff --git a/test/validator/samples/component-slot-default-duplicate.skip/errors.json b/test/validator/samples/component-slot-default-duplicate.skip/errors.json new file mode 100644 index 0000000000..10a7789168 --- /dev/null +++ b/test/validator/samples/component-slot-default-duplicate.skip/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "duplicate default element", + "loc": { + "line": 2, + "column": 0 + }, + "pos": 14 +}] \ No newline at end of file diff --git a/test/validator/samples/component-slot-default-duplicate.skip/input.html b/test/validator/samples/component-slot-default-duplicate.skip/input.html new file mode 100644 index 0000000000..9dffac4144 --- /dev/null +++ b/test/validator/samples/component-slot-default-duplicate.skip/input.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/test/validator/samples/component-slot-default-reserved/errors.json b/test/validator/samples/component-slot-default-reserved/errors.json new file mode 100644 index 0000000000..f951f61a5d --- /dev/null +++ b/test/validator/samples/component-slot-default-reserved/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "default is a reserved word — it cannot be used as a slot name", + "loc": { + "line": 1, + "column": 6 + }, + "pos": 6 +}] \ No newline at end of file diff --git a/test/validator/samples/component-slot-default-reserved/input.html b/test/validator/samples/component-slot-default-reserved/input.html new file mode 100644 index 0000000000..397ae0aaf5 --- /dev/null +++ b/test/validator/samples/component-slot-default-reserved/input.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/validator/samples/component-slot-dynamic-attribute/errors.json b/test/validator/samples/component-slot-dynamic-attribute/errors.json new file mode 100644 index 0000000000..3d88aa3367 --- /dev/null +++ b/test/validator/samples/component-slot-dynamic-attribute/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "slot attribute cannot have a dynamic value", + "loc": { + "line": 2, + "column": 9 + }, + "pos": 18 +}] \ No newline at end of file diff --git a/test/validator/samples/component-slot-dynamic-attribute/input.html b/test/validator/samples/component-slot-dynamic-attribute/input.html new file mode 100644 index 0000000000..1827eb2dd3 --- /dev/null +++ b/test/validator/samples/component-slot-dynamic-attribute/input.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/test/validator/samples/component-slot-dynamic/errors.json b/test/validator/samples/component-slot-dynamic/errors.json new file mode 100644 index 0000000000..38b45bb364 --- /dev/null +++ b/test/validator/samples/component-slot-dynamic/errors.json @@ -0,0 +1,8 @@ +[{ + "message": " name cannot be dynamic", + "loc": { + "line": 1, + "column": 6 + }, + "pos": 6 +}] \ No newline at end of file diff --git a/test/validator/samples/component-slot-dynamic/input.html b/test/validator/samples/component-slot-dynamic/input.html new file mode 100644 index 0000000000..4636cd4973 --- /dev/null +++ b/test/validator/samples/component-slot-dynamic/input.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/validator/samples/component-slot-each-block/errors.json b/test/validator/samples/component-slot-each-block/errors.json new file mode 100644 index 0000000000..c85d7fee58 --- /dev/null +++ b/test/validator/samples/component-slot-each-block/errors.json @@ -0,0 +1,8 @@ +[{ + "message": " cannot be a child of an each-block", + "loc": { + "line": 2, + "column": 1 + }, + "pos": 27 +}] \ No newline at end of file diff --git a/test/validator/samples/component-slot-each-block/input.html b/test/validator/samples/component-slot-each-block/input.html new file mode 100644 index 0000000000..2e4f7cb812 --- /dev/null +++ b/test/validator/samples/component-slot-each-block/input.html @@ -0,0 +1,3 @@ +{{#each things as thing}} + +{{/each}} \ No newline at end of file diff --git a/test/validator/samples/component-slot-named-duplicate.skip/errors.json b/test/validator/samples/component-slot-named-duplicate.skip/errors.json new file mode 100644 index 0000000000..535a049c0d --- /dev/null +++ b/test/validator/samples/component-slot-named-duplicate.skip/errors.json @@ -0,0 +1,8 @@ +[{ + "message": "duplicate 'foo' element", + "loc": { + "line": 2, + "column": 6 + }, + "pos": 31 +}] \ No newline at end of file diff --git a/test/validator/samples/component-slot-named-duplicate.skip/input.html b/test/validator/samples/component-slot-named-duplicate.skip/input.html new file mode 100644 index 0000000000..b0affc0608 --- /dev/null +++ b/test/validator/samples/component-slot-named-duplicate.skip/input.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/test/validator/samples/component-slot-nested/errors.json b/test/validator/samples/component-slot-nested/errors.json new file mode 100644 index 0000000000..9d19da7fd4 --- /dev/null +++ b/test/validator/samples/component-slot-nested/errors.json @@ -0,0 +1,8 @@ +[{ + "message": " elements cannot be nested", + "loc": { + "line": 2, + "column": 1 + }, + "pos": 8 +}] \ No newline at end of file diff --git a/test/validator/samples/component-slot-nested/input.html b/test/validator/samples/component-slot-nested/input.html new file mode 100644 index 0000000000..6365f729f4 --- /dev/null +++ b/test/validator/samples/component-slot-nested/input.html @@ -0,0 +1,3 @@ + + + \ No newline at end of file