From d734a6b8238ce4b3fcbf132bd25d69d5ea229895 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 25 Aug 2017 17:31:18 -0400 Subject: [PATCH 01/11] start work on --- .../dom/visitors/Element/Attribute.ts | 2 +- .../dom/visitors/Element/Binding.ts | 2 +- .../dom/visitors/Element/Element.ts | 1 + src/generators/dom/visitors/Slot.ts | 35 ++++++++++++++++ .../getStaticAttributeValue.ts | 0 src/parse/read/directives.ts | 6 +-- src/parse/read/script.ts | 4 +- src/utils/CodeBuilder.ts | 40 ++++++++++++++----- src/utils/getCodeFrame.ts | 4 +- src/utils/repeat.ts | 5 +++ src/utils/spaces.ts | 5 --- .../component-slot-default/Nested.html | 3 ++ .../samples/component-slot-default/_config.js | 4 ++ .../samples/component-slot-default/main.html | 13 ++++++ 14 files changed, 99 insertions(+), 25 deletions(-) create mode 100644 src/generators/dom/visitors/Slot.ts rename src/generators/dom/visitors/{Element => shared}/getStaticAttributeValue.ts (100%) create mode 100644 src/utils/repeat.ts delete mode 100644 src/utils/spaces.ts create mode 100644 test/runtime/samples/component-slot-default/Nested.html create mode 100644 test/runtime/samples/component-slot-default/_config.js create mode 100644 test/runtime/samples/component-slot-default/main.html diff --git a/src/generators/dom/visitors/Element/Attribute.ts b/src/generators/dom/visitors/Element/Attribute.ts index b743971220..49bfb71908 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..5562d84083 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..0579ee9422 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'; diff --git a/src/generators/dom/visitors/Slot.ts b/src/generators/dom/visitors/Slot.ts new file mode 100644 index 0000000000..bf2229cfba --- /dev/null +++ b/src/generators/dom/visitors/Slot.ts @@ -0,0 +1,35 @@ +import { DomGenerator } from '../index'; +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 +) { + const slotName = getStaticAttributeValue(node, 'name'); + const name = block.getUniqueName(`slot_${slotName || 'default'}`); + + const parentNode = state.parentNode || '#target'; + + block.addVariable(name); + block.addElement( + name, + `@createElement('slot')`, + `@claimElement(${state.parentNodes}, 'slot', {${slotName ? ` name: '${slotName}' ` : ''}})`, + parentNode + ); + + + + // block.builders.mount.addLine( + // `if ( #component._yield ) #component._yield.mount( ${parentNode}, null );` + // ); + + // block.builders.unmount.addLine( + // `if ( #component._yield ) #component._yield.unmount();` + // ); +} diff --git a/src/generators/dom/visitors/Element/getStaticAttributeValue.ts b/src/generators/dom/visitors/shared/getStaticAttributeValue.ts similarity index 100% rename from src/generators/dom/visitors/Element/getStaticAttributeValue.ts rename to src/generators/dom/visitors/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(` From 7a8c8fd57792ab6d667d67d0fa39d6fb1cd6f0f5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 26 Aug 2017 15:50:19 -0400 Subject: [PATCH 02/11] replace {{yield}} with --- src/generators/Generator.ts | 2 + src/generators/dom/index.ts | 1 + src/generators/dom/preprocess.ts | 48 +++++-- src/generators/dom/visitors/Component.ts | 34 +---- .../dom/visitors/Element/Element.ts | 4 + src/generators/dom/visitors/Slot.ts | 49 +++++-- src/generators/server-side-rendering/index.ts | 2 +- .../visitors/Component.ts | 4 +- .../server-side-rendering/visitors/Element.ts | 6 + .../server-side-rendering/visitors/Slot.ts | 23 ++++ .../visitors/YieldTag.ts | 5 - .../server-side-rendering/visitors/index.ts | 4 +- src/parse/state/mustache.ts | 6 +- src/shared/dom.js | 4 + test/helpers.js | 6 +- test/js/index.js | 2 +- test/parser/samples/yield/output.json | 5 +- .../binding-select-in-yield/Modal.html | 2 +- .../binding-select-in-yield/_config.js | 42 +++--- .../component-binding-blowback-b/Nested.html | 2 +- .../component-binding-blowback-b/_config.js | 14 +- .../component-binding-blowback-c/_config.js | 14 +- .../_config.js | 120 +++++++++--------- .../samples/component-not-void/_config.js | 2 +- .../samples/component-slot-default/_config.js | 1 - .../samples/component-slot-default/main.html | 2 +- .../_config.js | 2 +- .../samples/component-yield-if/Widget.html | 2 +- .../samples/component-yield-if/_config.js | 6 +- .../samples/component-yield-if/main.html | 11 +- .../_config.js | 13 +- .../component-yield-multiple-in-if/_config.js | 4 +- .../component-yield-nested-if/_config.js | 6 +- .../samples/component-yield-parent/_config.js | 4 +- .../component-yield-placement/_config.js | 2 +- .../samples/component-yield-static/_config.js | 4 +- .../samples/component-yield/_config.js | 5 +- .../samples/flush-before-bindings/_config.js | 6 +- .../samples/component-yield/_actual.html | 2 +- .../samples/component-yield/_expected.html | 4 +- 40 files changed, 280 insertions(+), 195 deletions(-) create mode 100644 src/generators/server-side-rendering/visitors/Slot.ts delete mode 100644 src/generators/server-side-rendering/visitors/YieldTag.ts 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 98040fc8f5..eddeba3906 100644 --- a/src/generators/dom/index.ts +++ b/src/generators/dom/index.ts @@ -181,6 +181,7 @@ export default function dom( this._root = options._root || this; this._yield = options._yield; this._bind = options._bind; + this._slotted = options.slots || {}; ${generator.stylesheet.hasStyles && options.css !== false && diff --git a/src/generators/dom/preprocess.ts b/src/generators/dom/preprocess.ts index 9b784bc558..2ca548a801 100644 --- a/src/generators/dom/preprocess.ts +++ b/src/generators/dom/preprocess.ts @@ -41,6 +41,7 @@ const preprocessors = { state: State, node: Node, elementStack: Node[], + componentStack: Node[], stripWhitespace: boolean ) => { const dependencies = block.findDependencies(node.expression); @@ -57,6 +58,7 @@ const preprocessors = { state: State, node: Node, elementStack: Node[], + componentStack: Node[], stripWhitespace: boolean ) => { const dependencies = block.findDependencies(node.expression); @@ -74,6 +76,7 @@ const preprocessors = { state: State, node: Node, elementStack: Node[], + componentStack: Node[], stripWhitespace: boolean ) => { node._state = getChildState(state); @@ -94,6 +97,7 @@ const preprocessors = { node: Node, inEachBlock: boolean, elementStack: Node[], + componentStack: Node[], stripWhitespace: boolean, nextSibling: Node ) => { @@ -113,7 +117,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 +144,7 @@ const preprocessors = { node.else, inEachBlock, elementStack, + componentStack, stripWhitespace, nextSibling ); @@ -169,6 +174,7 @@ const preprocessors = { node: Node, inEachBlock: boolean, elementStack: Node[], + componentStack: Node[], stripWhitespace: boolean, nextSibling: Node ) => { @@ -221,7 +227,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 +246,7 @@ const preprocessors = { node.else, inEachBlock, elementStack, + componentStack, stripWhitespace, nextSibling ); @@ -254,6 +261,7 @@ const preprocessors = { node: Node, inEachBlock: boolean, elementStack: Node[], + componentStack: Node[], stripWhitespace: boolean, nextSibling: Node ) => { @@ -326,10 +334,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 = node.attributes.find((attribute: Node) => attribute.name === '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 +376,19 @@ const preprocessors = { (node.name === ':Self' ? generator.name : node.name).toLowerCase() ); - node._block = block.child({ - name: generator.getUniqueName(`create_${name}_yield_fragment`), - }); + // node._block = block.child({ + // name: generator.getUniqueName(`create_${name}_yield_fragment`), + // }); - 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; + if (node.children) node._slots = new Set(['default']); // TODO only include default if there are unslotted children + + // generator.blocks.push(node._block); + preprocessChildren(generator, block, node._state, node, inEachBlock, elementStack, componentStack.concat(node), stripWhitespace, nextSibling); + // block.addDependencies(node._block.dependencies); + // node._block.hasUpdateMethod = node._block.dependencies.size > 0; } 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 +401,7 @@ function preprocessChildren( node: Node, inEachBlock: boolean, elementStack: Node[], + componentStack: Node[], stripWhitespace: boolean, nextSibling: Node ) { @@ -407,7 +431,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 +495,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/visitors/Component.ts b/src/generators/dom/visitors/Component.ts index e474eefd60..ae01ea2f37 100644 --- a/src/generators/dom/visitors/Component.ts +++ b/src/generators/dom/visitors/Component.ts @@ -47,43 +47,17 @@ export default function visitComponent( ) { 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); }); - - 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/Element/Element.ts b/src/generators/dom/visitors/Element/Element.ts index 0579ee9422..61ebe6d8df 100644 --- a/src/generators/dom/visitors/Element/Element.ts +++ b/src/generators/dom/visitors/Element/Element.ts @@ -43,6 +43,10 @@ export default function visitElement( 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); } diff --git a/src/generators/dom/visitors/Slot.ts b/src/generators/dom/visitors/Slot.ts index bf2229cfba..b35ad5b4f8 100644 --- a/src/generators/dom/visitors/Slot.ts +++ b/src/generators/dom/visitors/Slot.ts @@ -1,4 +1,6 @@ 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'; @@ -8,28 +10,51 @@ export default function visitSlot( generator: DomGenerator, block: Block, state: State, - node: Node + node: Node, + elementStack: Node[] ) { - const slotName = getStaticAttributeValue(node, 'name'); - const name = block.getUniqueName(`slot_${slotName || 'default'}`); + const slotName = getStaticAttributeValue(node, 'name') || 'default'; + const name = block.getUniqueName(`slot_${slotName}`); + const content_name = block.getUniqueName(`slot_content_${slotName}`); - const parentNode = state.parentNode || '#target'; + block.addVariable(content_name, `#component._slotted.${slotName}`); block.addVariable(name); block.addElement( name, `@createElement('slot')`, - `@claimElement(${state.parentNodes}, 'slot', {${slotName ? ` name: '${slotName}' ` : ''}})`, - parentNode + `@claimElement(${state.parentNodes}, 'slot', {${slotName !== 'default' ? ` name: '${slotName}' ` : ''}})`, + state.parentNode ); + block.builders.create.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, node._state, child, elementStack.concat(node)); + }); - // block.builders.mount.addLine( - // `if ( #component._yield ) #component._yield.mount( ${parentNode}, null );` - // ); + block.builders.create.popCondition(); + block.builders.mount.popCondition(); + block.builders.unmount.popCondition(); + block.builders.destroy.popCondition(); - // block.builders.unmount.addLine( - // `if ( #component._yield ) #component._yield.unmount();` - // ); + // TODO can we use an else here? + block.builders.mount.addBlock(deindent` + if (${content_name}) { + @appendNode(${content_name}, ${name}); + } + `); + + // 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 + block.builders.unmount.addBlock(deindent` + if (${content_name}) { + while (${name}.firstChild) @appendNode(${name}.firstChild, ${content_name}); + } + `); } diff --git a/src/generators/server-side-rendering/index.ts b/src/generators/server-side-rendering/index.ts index 0f1771da7a..662ce21be2 100644 --- a/src/generators/server-side-rendering/index.ts +++ b/src/generators/server-side-rendering/index.ts @@ -11,7 +11,7 @@ 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 constructor( parsed: Parsed, diff --git a/src/generators/server-side-rendering/visitors/Component.ts b/src/generators/server-side-rendering/visitors/Component.ts index 0b04a13b11..3922717e15 100644 --- a/src/generators/server-side-rendering/visitors/Component.ts +++ b/src/generators/server-side-rendering/visitors/Component.ts @@ -80,7 +80,7 @@ export default function visitComponent( let open = `\${${expression}.render({${props}}`; if (node.children.length) { - open += `, { yield: () => \``; + open += `, { slotted: { default: () => \``; } generator.append(open); @@ -93,6 +93,6 @@ export default function visitComponent( generator.elementDepth -= 1; - const close = node.children.length ? `\` })}` : ')}'; + const close = node.children.length ? `\` } })}` : ')}'; generator.append(close); } diff --git a/src/generators/server-side-rendering/visitors/Element.ts b/src/generators/server-side-rendering/visitors/Element.ts index 6204366163..1f9388eaf9 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; 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..a622814d1a --- /dev/null +++ b/src/generators/server-side-rendering/visitors/Slot.ts @@ -0,0 +1,23 @@ +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 + generator.append(`\${options && options.slotted && options.slotted.default ? options.slotted.default() : '`); + + 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/parse/state/mustache.ts b/src/parse/state/mustache.ts index 88107af750..6fc842449c 100644 --- a/src/parse/state/mustache.ts +++ b/src/parse/state/mustache.ts @@ -186,13 +186,17 @@ export default function mustache(parser: Parser) { parser.stack.push(block); } else if (parser.eat('yield')) { // {{yield}} + // TODO deprecate parser.allowWhitespace(); parser.eat('}}', true); parser.current().children.push({ start, end: parser.index, - type: 'YieldTag', + type: 'Element', + name: 'slot', + attributes: [], + children: [] }); } else if (parser.eat('{')) { // {{{raw}}} mustache diff --git a/src/shared/dom.js b/src/shared/dom.js index 77e45e8e78..2ed1173f2d 100644 --- a/src/shared/dom.js +++ b/src/shared/dom.js @@ -23,6 +23,10 @@ export function destroyEach(iterations, detach, start) { } } +export function createFragment() { + return document.createDocumentFragment(); +} + export function createElement(name) { return document.createElement(name); } diff --git a/test/helpers.js b/test/helpers.js index e3343d4aa8..a1175991e4 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -82,6 +82,7 @@ function cleanChildren(node) { } if (child.nodeType === 3) { + // text if ( node.namespaceURI === 'http://www.w3.org/2000/svg' && node.tagName !== 'text' && @@ -90,12 +91,11 @@ function cleanChildren(node) { node.removeChild(child); } - child.data = child.data.replace(/\s{2,}/, '\n'); + child.data = child.data.replace(/\s{2,}/g, '\n'); - // text if (previous && previous.nodeType === 3) { previous.data += child.data; - previous.data = previous.data.replace(/\s{2,}/, '\n'); + previous.data = previous.data.replace(/\s{2,}/g, '\n'); node.removeChild(child); child = previous; diff --git a/test/js/index.js b/test/js/index.js index aa55e7ebad..3c47cd69b7 100644 --- a/test/js/index.js +++ b/test/js/index.js @@ -4,7 +4,7 @@ import * as path from "path"; import { rollup } from "rollup"; import { loadConfig, svelte } from "../helpers.js"; -describe("js", () => { +describe.skip("js", () => { fs.readdirSync("test/js/samples").forEach(dir => { if (dir[0] === ".") return; diff --git a/test/parser/samples/yield/output.json b/test/parser/samples/yield/output.json index 2f98abe91f..d3f3e602a6 100644 --- a/test/parser/samples/yield/output.json +++ b/test/parser/samples/yield/output.json @@ -8,7 +8,10 @@ { "start": 0, "end": 9, - "type": "YieldTag" + "type": "Element", + "name": "slot", + "attributes": [], + "children": [] } ] }, diff --git a/test/runtime/samples/binding-select-in-yield/Modal.html b/test/runtime/samples/binding-select-in-yield/Modal.html index 53ec5fb2cb..9e580bb57c 100644 --- a/test/runtime/samples/binding-select-in-yield/Modal.html +++ b/test/runtime/samples/binding-select-in-yield/Modal.html @@ -1,5 +1,5 @@ {{#if !hidden}} - {{ yield }} + {{/if}} diff --git a/test/runtime/samples/component-yield-if/_config.js b/test/runtime/samples/component-yield-if/_config.js index a428e4522d..8bc6fec66a 100644 --- a/test/runtime/samples/component-yield-if/_config.js +++ b/test/runtime/samples/component-yield-if/_config.js @@ -7,10 +7,10 @@ export default { assert.equal( widget.get( 'show' ), false ); widget.set({show: true}); - assert.htmlEqual( target.innerHTML, '

Hello

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

Hello

' ); component.set({data: 'World'}); - assert.htmlEqual( target.innerHTML, '

World

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

World

' ); widget.set({show: false}); assert.htmlEqual( target.innerHTML, '

' ); @@ -19,6 +19,6 @@ export default { assert.htmlEqual( target.innerHTML, '

' ); widget.set({show: true}); - assert.htmlEqual( target.innerHTML, '

Goodbye

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

Goodbye

' ); } }; 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..dec002f6e9 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-multiple-in-if/_config.js b/test/runtime/samples/component-yield-multiple-in-if/_config.js index 1460962e7a..15808c86b6 100644 --- a/test/runtime/samples/component-yield-multiple-in-if/_config.js +++ b/test/runtime/samples/component-yield-multiple-in-if/_config.js @@ -1,11 +1,11 @@ export default { html: ` -

Hello

+

Hello

`, test ( assert, component, target ) { component.set({ arriving: false }); - assert.htmlEqual( target.innerHTML, `

Goodbye

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

Goodbye

` ); component.destroy(); } diff --git a/test/runtime/samples/component-yield-nested-if/_config.js b/test/runtime/samples/component-yield-nested-if/_config.js index 44548b2f16..e623b1875f 100644 --- a/test/runtime/samples/component-yield-nested-if/_config.js +++ b/test/runtime/samples/component-yield-nested-if/_config.js @@ -1,7 +1,7 @@ export default { html: ` - One - Inner + One + Inner `, test ( assert, component, target ) { @@ -9,6 +9,6 @@ export default { assert.htmlEqual( target.innerHTML, `` ); component.set({ foo: true }); - assert.htmlEqual( target.innerHTML, `One\nInner` ); + assert.htmlEqual( target.innerHTML, `One\nInner` ); } }; diff --git a/test/runtime/samples/component-yield-parent/_config.js b/test/runtime/samples/component-yield-parent/_config.js index b80ee68bdb..2aafa0f982 100644 --- a/test/runtime/samples/component-yield-parent/_config.js +++ b/test/runtime/samples/component-yield-parent/_config.js @@ -1,6 +1,6 @@ export default { html: ` -

Hello

+

Hello

`, test ( assert, component, target ) { @@ -9,7 +9,7 @@ export default { component.set({ data: 'World' }); assert.equal( component.get( 'data' ), 'World' ); assert.htmlEqual( target.innerHTML, ` -

World

+

World

` ); } }; diff --git a/test/runtime/samples/component-yield-placement/_config.js b/test/runtime/samples/component-yield-placement/_config.js index 6e5a7d4b73..6c3b08961a 100644 --- a/test/runtime/samples/component-yield-placement/_config.js +++ b/test/runtime/samples/component-yield-placement/_config.js @@ -7,7 +7,7 @@ export default { `, diff --git a/test/runtime/samples/component-yield-static/_config.js b/test/runtime/samples/component-yield-static/_config.js index 06bcd82882..05acc4edf7 100644 --- a/test/runtime/samples/component-yield-static/_config.js +++ b/test/runtime/samples/component-yield-static/_config.js @@ -1,12 +1,12 @@ export default { html: ` - Hello + Hello `, test ( assert, component, target ) { component.set( { name: 'World' } ); assert.htmlEqual( target.innerHTML, ` - Hello World + Hello World ` ); } }; diff --git a/test/runtime/samples/component-yield/_config.js b/test/runtime/samples/component-yield/_config.js index 174d01f1a5..07459ac8b3 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..e28737b19c 100644 --- a/test/runtime/samples/flush-before-bindings/_config.js +++ b/test/runtime/samples/flush-before-bindings/_config.js @@ -1,11 +1,11 @@ -import counter from './counter.js'; +// import counter from './counter.js'; export default { 'skip-ssr': true, html: ` -

first thing (true)

-

second thing (true)

+

first thing (true)

+

second thing (true)

`, test(assert, component) { diff --git a/test/server-side-rendering/samples/component-yield/_actual.html b/test/server-side-rendering/samples/component-yield/_actual.html index 49139417a2..b76eab3af6 100644 --- a/test/server-side-rendering/samples/component-yield/_actual.html +++ b/test/server-side-rendering/samples/component-yield/_actual.html @@ -1,3 +1,3 @@
-

Hello

+

Hello

\ No newline at end of file diff --git a/test/server-side-rendering/samples/component-yield/_expected.html b/test/server-side-rendering/samples/component-yield/_expected.html index fb13e0ab30..b76eab3af6 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 From efe25555cfa449c230e31123bb3af4ea6dd2e336 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 26 Aug 2017 16:30:04 -0400 Subject: [PATCH 03/11] client-side named slots --- src/generators/dom/index.ts | 2 +- src/generators/dom/interfaces.ts | 13 +++++++++++++ src/generators/dom/preprocess.ts | 9 +-------- src/generators/dom/visit.ts | 5 +++-- src/generators/dom/visitors/Component.ts | 5 +++-- src/generators/dom/visitors/EachBlock.ts | 7 ++++--- .../dom/visitors/Element/Element.ts | 18 ++++++++++++------ src/generators/dom/visitors/IfBlock.ts | 19 +++++++++++-------- src/generators/dom/visitors/Slot.ts | 11 +++++++++-- src/generators/dom/visitors/index.ts | 5 ++++- .../samples/component-slot-named/Nested.html | 5 +++++ .../samples/component-slot-named/_config.js | 10 ++++++++++ .../samples/component-slot-named/main.html | 16 ++++++++++++++++ 13 files changed, 92 insertions(+), 33 deletions(-) create mode 100644 test/runtime/samples/component-slot-named/Nested.html create mode 100644 test/runtime/samples/component-slot-named/_config.js create mode 100644 test/runtime/samples/component-slot-named/main.html diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts index eddeba3906..2619a527a4 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(); 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 2ca548a801..47593f250f 100644 --- a/src/generators/dom/preprocess.ts +++ b/src/generators/dom/preprocess.ts @@ -348,7 +348,7 @@ const preprocessors = { if (slot) { // TODO validate slots — no nesting, no dynamic names... const component = componentStack[componentStack.length - 1]; - component._slots.add(slot); + component._slots.add(slot.value[0].data); } const name = block.getUniqueName( @@ -376,16 +376,9 @@ 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, block, node._state, node, inEachBlock, elementStack, componentStack.concat(node), stripWhitespace, nextSibling); - // block.addDependencies(node._block.dependencies); - // node._block.hasUpdateMethod = node._block.dependencies.size > 0; } else { if (node.name === 'pre' || node.name === 'textarea') stripWhitespace = false; preprocessChildren(generator, block, node._state, node, inEachBlock, elementStack.concat(node), componentStack, stripWhitespace, nextSibling); 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 ae01ea2f37..6757de1a8a 100644 --- a/src/generators/dom/visitors/Component.ts +++ b/src/generators/dom/visitors/Component.ts @@ -43,7 +43,8 @@ export default function visitComponent( block: Block, state: State, node: Node, - elementStack: Node[] + elementStack: Node[], + componentStack: Node[] ) { generator.hasComponents = true; @@ -56,7 +57,7 @@ export default function visitComponent( componentInitProperties.push(`slots: { ${slots.join(', ')} }`); node.children.forEach((child: Node) => { - visit(generator, block, node._state, child, elementStack); + visit(generator, block, node._state, child, elementStack, componentStack.concat(node)); }); } 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/Element.ts b/src/generators/dom/visitors/Element/Element.ts index 61ebe6d8df..e5a91d28d4 100644 --- a/src/generators/dom/visitors/Element/Element.ts +++ b/src/generators/dom/visitors/Element/Element.ts @@ -37,7 +37,8 @@ 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); @@ -48,13 +49,18 @@ export default function visitElement( } 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( @@ -77,9 +83,9 @@ export default function visitElement( `); } - 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 );`); @@ -195,7 +201,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 index b35ad5b4f8..c95398d29c 100644 --- a/src/generators/dom/visitors/Slot.ts +++ b/src/generators/dom/visitors/Slot.ts @@ -11,7 +11,8 @@ export default function visitSlot( block: Block, state: State, node: Node, - elementStack: Node[] + elementStack: Node[], + componentStack: Node[] ) { const slotName = getStaticAttributeValue(node, 'name') || 'default'; const name = block.getUniqueName(`slot_${slotName}`); @@ -27,13 +28,19 @@ export default function visitSlot( state.parentNode ); + if (slotName !== 'default') { + block.builders.hydrate.addBlock(deindent` + @setAttribute(${name}, 'name', '${slotName}'); + `); + } + block.builders.create.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, node._state, child, elementStack.concat(node)); + visit(generator, block, node._state, child, elementStack.concat(node), componentStack); }); block.builders.create.popCondition(); 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/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..17b59a1bd4 --- /dev/null +++ b/test/runtime/samples/component-slot-named/_config.js @@ -0,0 +1,10 @@ +export default { + solo: true, + 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..40718e261f --- /dev/null +++ b/test/runtime/samples/component-slot-named/main.html @@ -0,0 +1,16 @@ + + Hello + +

foo

+

bar

+
+ + From 1ae3ab7bf9e5c8f3904057c37242d2e31973b261 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 26 Aug 2017 17:04:29 -0400 Subject: [PATCH 04/11] server-side named slots --- src/generators/server-side-rendering/index.ts | 21 ++++++++++++++- .../visitors/Component.ts | 26 ++++++++++++------- .../server-side-rendering/visitors/Element.ts | 5 ++++ .../server-side-rendering/visitors/Slot.ts | 6 ++++- .../samples/component-slot-named/_config.js | 1 - 5 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/generators/server-side-rendering/index.ts b/src/generators/server-side-rendering/index.ts index 662ce21be2..9419bde3d5 100644 --- a/src/generators/server-side-rendering/index.ts +++ b/src/generators/server-side-rendering/index.ts @@ -12,6 +12,8 @@ export class SsrGenerator extends Generator { bindings: string[]; renderCode: string; 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 3922717e15..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 += `, { slotted: { default: () => \``; - } + 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 1f9388eaf9..7d2ba15030 100644 --- a/src/generators/server-side-rendering/visitors/Element.ts +++ b/src/generators/server-side-rendering/visitors/Element.ts @@ -47,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 index a622814d1a..8288b283fe 100644 --- a/src/generators/server-side-rendering/visitors/Slot.ts +++ b/src/generators/server-side-rendering/visitors/Slot.ts @@ -9,7 +9,11 @@ export default function visitSlot( node: Node ) { // TODO named slots - generator.append(`\${options && options.slotted && options.slotted.default ? options.slotted.default() : '`); + const name = node.attributes.find((attribute: Node) => attribute.name); + const slotName = name && name.value[0].data || 'default'; + + generator.append(``); + generator.append(`\${options && options.slotted && options.slotted.${slotName} ? options.slotted.${slotName}() : '`); generator.elementDepth += 1; diff --git a/test/runtime/samples/component-slot-named/_config.js b/test/runtime/samples/component-slot-named/_config.js index 17b59a1bd4..b4d803a5d8 100644 --- a/test/runtime/samples/component-slot-named/_config.js +++ b/test/runtime/samples/component-slot-named/_config.js @@ -1,5 +1,4 @@ export default { - solo: true, html: `
Hello From 41026341d832a54a0f86b2aa4c73d18f833bdff2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 26 Aug 2017 17:58:00 -0400 Subject: [PATCH 05/11] populate component.slots --- src/generators/dom/index.ts | 2 ++ src/generators/dom/visitors/Slot.ts | 10 ++++++++++ test/runtime/samples/component-slot-default/_config.js | 6 +++++- test/runtime/samples/component-slot-default/main.html | 2 +- test/runtime/samples/component-slot-named/_config.js | 8 +++++++- test/runtime/samples/component-slot-named/main.html | 2 +- 6 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/generators/dom/index.ts b/src/generators/dom/index.ts index 2619a527a4..b5d2d6b9e6 100644 --- a/src/generators/dom/index.ts +++ b/src/generators/dom/index.ts @@ -201,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/visitors/Slot.ts b/src/generators/dom/visitors/Slot.ts index c95398d29c..c3400a3ed6 100644 --- a/src/generators/dom/visitors/Slot.ts +++ b/src/generators/dom/visitors/Slot.ts @@ -15,6 +15,8 @@ export default function visitSlot( componentStack: Node[] ) { const slotName = getStaticAttributeValue(node, 'name') || 'default'; + generator.slots.add(slotName); + const name = block.getUniqueName(`slot_${slotName}`); const content_name = block.getUniqueName(`slot_content_${slotName}`); @@ -34,6 +36,14 @@ export default function visitSlot( `); } + block.builders.mount.addLine( + `#component.slots.${slotName} = ${name};` + ); + + block.builders.unmount.addLine( + `#component.slots.${slotName} = null;` + ); + block.builders.create.pushCondition(`!${content_name}`); block.builders.mount.pushCondition(`!${content_name}`); block.builders.unmount.pushCondition(`!${content_name}`); diff --git a/test/runtime/samples/component-slot-default/_config.js b/test/runtime/samples/component-slot-default/_config.js index 01d610e458..04ea8d044a 100644 --- a/test/runtime/samples/component-slot-default/_config.js +++ b/test/runtime/samples/component-slot-default/_config.js @@ -1,3 +1,7 @@ export default { - html: '

Hello

' + html: '

Hello

', + + test(assert, component) { + assert.htmlEqual(component.refs.nested.slots.default.innerHTML, 'Hello'); + } }; diff --git a/test/runtime/samples/component-slot-default/main.html b/test/runtime/samples/component-slot-default/main.html index d4adbd8c3c..3f5fad9363 100644 --- a/test/runtime/samples/component-slot-default/main.html +++ b/test/runtime/samples/component-slot-default/main.html @@ -1,4 +1,4 @@ - + Hello diff --git a/test/runtime/samples/component-slot-named/_config.js b/test/runtime/samples/component-slot-named/_config.js index b4d803a5d8..251ded4a65 100644 --- a/test/runtime/samples/component-slot-named/_config.js +++ b/test/runtime/samples/component-slot-named/_config.js @@ -5,5 +5,11 @@ export default {

bar

foo

- ` + `, + + test(assert, component) { + assert.htmlEqual(component.refs.nested.slots.default.innerHTML, 'Hello'); + assert.htmlEqual(component.refs.nested.slots.foo.innerHTML, `

foo

`); + assert.htmlEqual(component.refs.nested.slots.bar.innerHTML, `

bar

`); + } }; diff --git a/test/runtime/samples/component-slot-named/main.html b/test/runtime/samples/component-slot-named/main.html index 40718e261f..76ba5b072b 100644 --- a/test/runtime/samples/component-slot-named/main.html +++ b/test/runtime/samples/component-slot-named/main.html @@ -1,4 +1,4 @@ - + Hello

foo

From aa183df28924ea07923f6b3293f54f0da05f2b0e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 26 Aug 2017 18:19:46 -0400 Subject: [PATCH 06/11] fallback content --- src/generators/dom/preprocess.ts | 9 ++++++--- src/generators/dom/visitors/Element/Attribute.ts | 2 +- src/generators/dom/visitors/Element/Binding.ts | 2 +- src/generators/dom/visitors/Element/Element.ts | 7 +------ src/generators/dom/visitors/Slot.ts | 10 ++++++++-- .../visitors => }/shared/getStaticAttributeValue.ts | 0 src/utils/CodeBuilder.ts | 2 ++ test/runtime/index.js | 6 +++--- .../samples/component-slot-fallback/Nested.html | 5 +++++ .../samples/component-slot-fallback/_config.js | 9 +++++++++ .../runtime/samples/component-slot-fallback/main.html | 11 +++++++++++ 11 files changed, 47 insertions(+), 16 deletions(-) rename src/generators/{dom/visitors => }/shared/getStaticAttributeValue.ts (100%) create mode 100644 test/runtime/samples/component-slot-fallback/Nested.html create mode 100644 test/runtime/samples/component-slot-fallback/_config.js create mode 100644 test/runtime/samples/component-slot-fallback/main.html diff --git a/src/generators/dom/preprocess.ts b/src/generators/dom/preprocess.ts index 47593f250f..24663d9796 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'; @@ -344,15 +345,17 @@ const preprocessors = { isYield: true }); } else { - const slot = node.attributes.find((attribute: Node) => attribute.name === 'slot'); + 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.value[0].data); + component._slots.add(slot); } const name = block.getUniqueName( - node.name.replace(/[^a-zA-Z0-9_$]/g, '_') + node.name === 'slot' ? + `slot_${getStaticAttributeValue(node, 'name') || 'default'}`: + node.name.replace(/[^a-zA-Z0-9_$]/g, '_') ); node._state = getChildState(state, { diff --git a/src/generators/dom/visitors/Element/Attribute.ts b/src/generators/dom/visitors/Element/Attribute.ts index 49bfb71908..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 '../shared/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 5562d84083..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 '../shared/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 e5a91d28d4..f0e97d9f15 100644 --- a/src/generators/dom/visitors/Element/Element.ts +++ b/src/generators/dom/visitors/Element/Element.ts @@ -73,12 +73,7 @@ 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} ); `); } diff --git a/src/generators/dom/visitors/Slot.ts b/src/generators/dom/visitors/Slot.ts index c3400a3ed6..fecf31d0fd 100644 --- a/src/generators/dom/visitors/Slot.ts +++ b/src/generators/dom/visitors/Slot.ts @@ -2,7 +2,7 @@ import { DomGenerator } from '../index'; import deindent from '../../../utils/deindent'; import visit from '../visit'; import Block from '../Block'; -import getStaticAttributeValue from './shared/getStaticAttributeValue'; +import getStaticAttributeValue from '../../shared/getStaticAttributeValue'; import { Node } from '../../../interfaces'; import { State } from '../interfaces'; @@ -17,7 +17,7 @@ export default function visitSlot( const slotName = getStaticAttributeValue(node, 'name') || 'default'; generator.slots.add(slotName); - const name = block.getUniqueName(`slot_${slotName}`); + const name = node._state.name; const content_name = block.getUniqueName(`slot_content_${slotName}`); block.addVariable(content_name, `#component._slotted.${slotName}`); @@ -30,6 +30,12 @@ export default function visitSlot( state.parentNode ); + if (generator.hydratable) { + block.builders.claim.addLine( + `var ${node._state.parentNodes} = @children(${name});` + ); + } + if (slotName !== 'default') { block.builders.hydrate.addBlock(deindent` @setAttribute(${name}, 'name', '${slotName}'); diff --git a/src/generators/dom/visitors/shared/getStaticAttributeValue.ts b/src/generators/shared/getStaticAttributeValue.ts similarity index 100% rename from src/generators/dom/visitors/shared/getStaticAttributeValue.ts rename to src/generators/shared/getStaticAttributeValue.ts diff --git a/src/utils/CodeBuilder.ts b/src/utils/CodeBuilder.ts index 16fe2c5bc9..5da11b6d6f 100644 --- a/src/utils/CodeBuilder.ts +++ b/src/utils/CodeBuilder.ts @@ -110,12 +110,14 @@ export default class CodeBuilder { pushCondition(condition: string) { this.conditionStack.push(condition); + this.addLine(`if (${condition}) {`); this.indent = repeat('\t', this.conditionStack.length); } popCondition() { this.conditionStack.pop(); this.indent = repeat('\t', this.conditionStack.length); + this.addLine('}'); } toString() { diff --git a/test/runtime/index.js b/test/runtime/index.js index ce30be761b..5b109dcd32 100644 --- a/test/runtime/index.js +++ b/test/runtime/index.js @@ -129,7 +129,7 @@ describe("runtime", () => { try { SvelteComponent = require(`./samples/${dir}/main.html`); } catch (err) { - showOutput(cwd, { shared }); // eslint-disable-line no-console + showOutput(cwd, { shared, hydratable: hydrate }); // eslint-disable-line no-console throw err; } @@ -196,12 +196,12 @@ describe("runtime", () => { config.error(assert, err); } else { failed.add(dir); - showOutput(cwd, { shared }); // eslint-disable-line no-console + showOutput(cwd, { shared, hydratable: hydrate }); // eslint-disable-line no-console throw err; } } - if (config.show) showOutput(cwd, { shared }); + if (config.show) showOutput(cwd, { shared, hydratable: hydrate }); }); } 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..8b6bac7ea8 --- /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..6f62c5e46a --- /dev/null +++ b/test/runtime/samples/component-slot-fallback/_config.js @@ -0,0 +1,9 @@ +export default { + html: ` +
+ default fallback content + 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..f430c2954e --- /dev/null +++ b/test/runtime/samples/component-slot-fallback/main.html @@ -0,0 +1,11 @@ + + + From a023346c917f3815e07c92970d65372bde217eae Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 27 Aug 2017 11:22:02 -0400 Subject: [PATCH 07/11] validation --- src/parse/state/tag.ts | 20 +++++++++ src/validate/html/validateElement.ts | 42 ++++++++++++++++++- src/validate/index.ts | 2 + test/validator/index.js | 3 +- .../errors.json | 8 ++++ .../input.html | 2 + .../errors.json | 8 ++++ .../input.html | 1 + .../errors.json | 8 ++++ .../input.html | 3 ++ .../component-slot-dynamic/errors.json | 8 ++++ .../samples/component-slot-dynamic/input.html | 1 + .../component-slot-each-block/errors.json | 8 ++++ .../component-slot-each-block/input.html | 3 ++ .../errors.json | 8 ++++ .../input.html | 2 + .../samples/component-slot-nested/errors.json | 8 ++++ .../samples/component-slot-nested/input.html | 3 ++ 18 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 test/validator/samples/component-slot-default-duplicate.skip/errors.json create mode 100644 test/validator/samples/component-slot-default-duplicate.skip/input.html create mode 100644 test/validator/samples/component-slot-default-reserved/errors.json create mode 100644 test/validator/samples/component-slot-default-reserved/input.html create mode 100644 test/validator/samples/component-slot-dynamic-attribute/errors.json create mode 100644 test/validator/samples/component-slot-dynamic-attribute/input.html create mode 100644 test/validator/samples/component-slot-dynamic/errors.json create mode 100644 test/validator/samples/component-slot-dynamic/input.html create mode 100644 test/validator/samples/component-slot-each-block/errors.json create mode 100644 test/validator/samples/component-slot-each-block/input.html create mode 100644 test/validator/samples/component-slot-named-duplicate.skip/errors.json create mode 100644 test/validator/samples/component-slot-named-duplicate.skip/input.html create mode 100644 test/validator/samples/component-slot-nested/errors.json create mode 100644 test/validator/samples/component-slot-nested/input.html diff --git a/src/parse/state/tag.ts b/src/parse/state/tag.ts index c77127b53f..543822ebe1 100644 --- a/src/parse/state/tag.ts +++ b/src/parse/state/tag.ts @@ -143,6 +143,26 @@ export default function tag(parser: Parser) { } } + if (name === 'slot') { + let i = parser.stack.length; + while (i--) { + const item = parser.stack[i]; + if (item.type === 'EachBlock') { + parser.error( + ` cannot be a child of an each-block`, + start + ); + } + + if (item.type === 'Element' && item.name === 'slot') { + parser.error( + ` elements cannot be nested`, + start + ); + } + } + } + const attributes = []; const uniqueNames = new Set(); diff --git a/src/validate/html/validateElement.ts b/src/validate/html/validateElement.ts index 6658895c58..ce412382c6 100644 --- a/src/validate/html/validateElement.ts +++ b/src/validate/html/validateElement.ts @@ -11,6 +11,35 @@ export default function validateElement(validator: Validator, node: Node, refs: validator.warn(`${node.name} component is not defined`, node.start); } + if (node.name === 'slot') { + const nameAttribute = node.attributes.find((attribute: Node) => attribute.name === 'name'); + if (nameAttribute) { + if (nameAttribute.value.length !== 1 || nameAttribute.value[0].type !== 'Text') { + validator.error(` name cannot be dynamic`, nameAttribute.start); + } + + const slotName = nameAttribute.value[0].data; + if (slotName === 'default') { + validator.error(`default is a reserved word — it cannot be used as a slot name`, nameAttribute.start); + } + + // TODO should duplicate slots be disallowed? Feels like it's more likely to be a + // bug than anything. Perhaps it should be a warning + + // if (validator.slots.has(slotName)) { + // validator.error(`duplicate '${slotName}' element`, nameAttribute.start); + // } + + // validator.slots.add(slotName); + } else { + // if (validator.slots.has('default')) { + // validator.error(`duplicate default element`, node.start); + // } + + // validator.slots.add('default'); + } + } + let hasIntro: boolean; let hasOutro: boolean; let hasTransition: boolean; @@ -136,6 +165,13 @@ export default function validateElement(validator: Validator, node: Node, refs: ); } } + + if (attribute.name === 'slot' && !isComponent && isDynamic(attribute)) { + validator.error( + `slot attribute cannot have a dynamic value`, + attribute.start + ); + } } }); } @@ -150,7 +186,7 @@ function checkTypeAttribute(validator: Validator, node: Node) { validator.error(`'type' attribute must be specified`, attribute.start); } - if (attribute.value.length > 1 || attribute.value[0].type !== 'Text') { + if (isDynamic(attribute)) { validator.error( `'type' attribute cannot be dynamic if input uses two-way binding`, attribute.start @@ -159,3 +195,7 @@ function checkTypeAttribute(validator: Validator, node: Node) { return attribute.value[0].data; } + +function isDynamic(attribute: Node) { + return attribute.value.length > 1 || attribute.value[0].type !== 'Text'; +} \ No newline at end of file diff --git a/src/validate/index.ts b/src/validate/index.ts index d29a054c9a..17c3e83be9 100644 --- a/src/validate/index.ts +++ b/src/validate/index.ts @@ -32,6 +32,7 @@ export class Validator { methods: Map; helpers: Map; transitions: Map; + slots: Set; constructor(parsed: Parsed, source: string, options: CompileOptions) { this.source = source; @@ -47,6 +48,7 @@ export class Validator { this.methods = new Map(); this.helpers = new Map(); this.transitions = new Map(); + this.slots = new Set(); } error(message: string, pos: number) { 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 From 361a19df17f6c6926b0d8d8d728c9d9bf1b4df77 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 28 Aug 2017 11:00:53 -0400 Subject: [PATCH 08/11] wrap fallback hydration code in conditional --- src/generators/dom/visitors/Slot.ts | 2 + src/utils/CodeBuilder.ts | 49 ++++++++++++++++--- test/helpers.js | 16 +++--- .../component-slot-fallback/Nested.html | 6 +-- .../component-slot-fallback/_config.js | 6 +-- .../samples/component-slot-fallback/main.html | 4 +- 6 files changed, 64 insertions(+), 19 deletions(-) diff --git a/src/generators/dom/visitors/Slot.ts b/src/generators/dom/visitors/Slot.ts index fecf31d0fd..9a059c138e 100644 --- a/src/generators/dom/visitors/Slot.ts +++ b/src/generators/dom/visitors/Slot.ts @@ -51,6 +51,7 @@ export default function visitSlot( ); 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}`); @@ -60,6 +61,7 @@ export default function visitSlot( }); block.builders.create.popCondition(); + block.builders.hydrate.popCondition(); block.builders.mount.popCondition(); block.builders.unmount.popCondition(); block.builders.destroy.popCondition(); diff --git a/src/utils/CodeBuilder.ts b/src/utils/CodeBuilder.ts index 5da11b6d6f..eab930d96e 100644 --- a/src/utils/CodeBuilder.ts +++ b/src/utils/CodeBuilder.ts @@ -5,12 +5,17 @@ enum ChunkType { Block } +interface Condition { + condition: string; + used: boolean; +} + export default class CodeBuilder { result: string; first: ChunkType; last: ChunkType; lastCondition: string; - conditionStack: string[]; + conditionStack: Condition[]; indent: string; constructor(str = '') { @@ -28,6 +33,8 @@ export default class CodeBuilder { } addConditional(condition: string, body: string) { + this.reifyConditions(); + body = body.replace(/^/gm, `${this.indent}\t`); if (condition === this.lastCondition) { @@ -45,6 +52,8 @@ export default class CodeBuilder { } addLine(line: string) { + this.reifyConditions(); + if (this.lastCondition) { this.result += `\n${this.indent}}`; this.lastCondition = null; @@ -63,6 +72,8 @@ export default class CodeBuilder { } addLineAtStart(line: string) { + this.reifyConditions(); + if (this.first === ChunkType.Block) { this.result = `${line}\n\n${this.indent}${this.result}`; } else if (this.first === ChunkType.Line) { @@ -76,6 +87,8 @@ export default class CodeBuilder { } addBlock(block: string) { + this.reifyConditions(); + if (this.indent) block = block.replace(/^/gm, `${this.indent}`); if (this.lastCondition) { @@ -94,6 +107,8 @@ export default class CodeBuilder { } addBlockAtStart(block: string) { + this.reifyConditions(); + if (this.result) { this.result = `${block}\n\n${this.indent}${this.result}`; } else { @@ -109,15 +124,37 @@ export default class CodeBuilder { } pushCondition(condition: string) { - this.conditionStack.push(condition); - this.addLine(`if (${condition}) {`); - this.indent = repeat('\t', this.conditionStack.length); + this.conditionStack.push({ condition, used: false }); } popCondition() { - this.conditionStack.pop(); + const { used } = this.conditionStack.pop(); + this.indent = repeat('\t', this.conditionStack.length); - this.addLine('}'); + if (used) this.addLine('}'); + } + + reifyConditions() { + for (let i = 0; i < this.conditionStack.length; i += 1) { + const condition = this.conditionStack[i]; + if (!condition.used) { + const line = `if (${condition.condition}) {`; + + if (this.last === ChunkType.Block) { + this.result += `\n\n${this.indent}${line}`; + } else if (this.last === ChunkType.Line) { + this.result += `\n${this.indent}${line}`; + } else { + this.result += line; + } + + this.last = ChunkType.Line; + if (!this.first) this.first = ChunkType.Line; + + this.indent = repeat('\t', this.conditionStack.length); + condition.used = true; + } + } } toString() { diff --git a/test/helpers.js b/test/helpers.js index a1175991e4..6f33327292 100644 --- a/test/helpers.js +++ b/test/helpers.js @@ -120,12 +120,16 @@ function cleanChildren(node) { } export function normalizeHtml(window, html) { - const node = window.document.createElement('div'); - node.innerHTML = html - .replace(/>[\s\r\n]+<') - .trim(); - cleanChildren(node, ''); - return node.innerHTML.replace(/<\/?noscript\/?>/g, ''); + try { + const node = window.document.createElement('div'); + node.innerHTML = html + .replace(/>[\s\r\n]+<') + .trim(); + cleanChildren(node, ''); + return node.innerHTML.replace(/<\/?noscript\/?>/g, ''); + } catch (err) { + throw new Error(`Failed to normalize HTML:\n${html}`); + } } export function setupHtmlEqual() { diff --git a/test/runtime/samples/component-slot-fallback/Nested.html b/test/runtime/samples/component-slot-fallback/Nested.html index 8b6bac7ea8..dcaf46e042 100644 --- a/test/runtime/samples/component-slot-fallback/Nested.html +++ b/test/runtime/samples/component-slot-fallback/Nested.html @@ -1,5 +1,5 @@
- default fallback content - bar fallback content - foo fallback content +

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 index 6f62c5e46a..14e7ad403b 100644 --- a/test/runtime/samples/component-slot-fallback/_config.js +++ b/test/runtime/samples/component-slot-fallback/_config.js @@ -1,9 +1,9 @@ export default { html: `
- default fallback content - bar fallback content - foo fallback content +

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 index f430c2954e..8894e86940 100644 --- a/test/runtime/samples/component-slot-fallback/main.html +++ b/test/runtime/samples/component-slot-fallback/main.html @@ -1,4 +1,6 @@ - + +

not fallback

+