first crack at slot context

pull/1998/head
Richard Harris 7 years ago
parent d4036ada85
commit 1cb63ed16f

@ -14,6 +14,8 @@ import mapChildren from './shared/mapChildren';
import { dimensions } from '../../utils/patterns';
import fuzzymatch from '../../utils/fuzzymatch';
import list from '../../utils/list';
import Let from './Let';
import TemplateScope from './shared/TemplateScope';
const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|switch|symbol|text|textPath|tref|tspan|unknown|use|view|vkern)$/;
@ -81,11 +83,13 @@ export default class Element extends Node {
bindings: Binding[] = [];
classes: Class[] = [];
handlers: EventHandler[] = [];
lets: Let[] = [];
intro?: Transition = null;
outro?: Transition = null;
animation?: Animation = null;
children: Node[];
namespace: string;
scope: TemplateScope;
constructor(component, parent, scope, info: any) {
super(component, parent, scope, info);
@ -168,6 +172,10 @@ export default class Element extends Node {
this.handlers.push(new EventHandler(component, this, scope, node));
break;
case 'Let':
this.lets.push(new Let(component, this, scope, node));
break;
case 'Transition':
const transition = new Transition(component, this, scope, node);
if (node.intro) this.intro = transition;
@ -183,7 +191,21 @@ export default class Element extends Node {
}
});
this.children = mapChildren(component, this, scope, info.children);
if (this.lets.length > 0) {
this.scope = scope.child();
this.lets.forEach(l => {
const dependencies = new Set([l.name]);
l.names.forEach(name => {
this.scope.add(name, dependencies);
});
});
} else {
this.scope = scope;
}
this.children = mapChildren(component, this, this.scope, info.children);
this.validate();

@ -5,15 +5,19 @@ import Binding from './Binding';
import EventHandler from './EventHandler';
import Expression from './shared/Expression';
import Component from '../Component';
import Let from './Let';
import TemplateScope from './shared/TemplateScope';
export default class InlineComponent extends Node {
type: 'InlineComponent';
name: string;
expression: Expression;
attributes: Attribute[];
bindings: Binding[];
handlers: EventHandler[];
attributes: Attribute[] = [];
bindings: Binding[] = [];
handlers: EventHandler[] = [];
lets: Let[] = [];
children: Node[];
scope: TemplateScope;
constructor(component: Component, parent, scope, info) {
super(component, parent, scope, info);
@ -29,10 +33,6 @@ export default class InlineComponent extends Node {
? new Expression(component, this, scope, info.expression)
: null;
this.attributes = [];
this.bindings = [];
this.handlers = [];
info.attributes.forEach(node => {
switch (node.type) {
case 'Action':
@ -60,6 +60,10 @@ export default class InlineComponent extends Node {
this.handlers.push(new EventHandler(component, this, scope, node));
break;
case 'Let':
this.lets.push(new Let(component, this, scope, node));
break;
case 'Transition':
component.error(node, {
code: `invalid-transition`,
@ -71,6 +75,20 @@ export default class InlineComponent extends Node {
}
});
this.children = mapChildren(component, this, scope, info.children);
if (this.lets.length > 0) {
this.scope = scope.child();
this.lets.forEach(l => {
const dependencies = new Set([l.name]);
l.names.forEach(name => {
this.scope.add(name, dependencies);
});
});
} else {
this.scope = scope;
}
this.children = mapChildren(component, this, this.scope, info.children);
}
}

@ -0,0 +1,27 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import Component from '../Component';
class Pattern {
constructor(node) {
// TODO implement `let:foo={bar}` and `let:contact={{ name, address }}` etc
}
}
export default class Let extends Node {
type: 'Let';
name: string;
pattern: Pattern;
names: string[];
constructor(component: Component, parent, scope, info) {
super(component, parent, scope, info);
this.name = info.name;
this.pattern = info.expression && new Pattern(info.expression);
// TODO
this.names = [this.name];
}
}

@ -19,26 +19,28 @@ export default class Slot extends Element {
});
}
if (attr.name !== 'name') {
component.error(attr, {
code: `invalid-slot-attribute`,
message: `"name" is the only attribute permitted on <slot> elements`
});
}
// if (attr.name !== 'name') {
// component.error(attr, {
// code: `invalid-slot-attribute`,
// message: `"name" is the only attribute permitted on <slot> elements`
// });
// }
if (attr.value.length !== 1 || attr.value[0].type !== 'Text') {
component.error(attr, {
code: `dynamic-slot-name`,
message: `<slot> name cannot be dynamic`
});
}
if (attr.name === 'name') {
if (attr.value.length !== 1 || attr.value[0].type !== 'Text') {
component.error(attr, {
code: `dynamic-slot-name`,
message: `<slot> name cannot be dynamic`
});
}
const slotName = attr.value[0].data;
if (slotName === 'default') {
component.error(attr, {
code: `invalid-slot-name`,
message: `default is a reserved word — it cannot be used as a slot name`
});
const slotName = attr.value[0].data;
if (slotName === 'default') {
component.error(attr, {
code: `invalid-slot-name`,
message: `default is a reserved word — it cannot be used as a slot name`
});
}
}
// TODO should duplicate slots be disallowed? Feels like it's more likely to be a

@ -130,14 +130,22 @@ export default class ElementWrapper extends Wrapper {
if (owner && owner.node.type === 'InlineComponent') {
const name = attribute.getStaticValue();
this.slot_block = block.child({
comment: createDebuggingComment(node, this.renderer.component),
name: this.renderer.component.getUniqueName(`create_${sanitize(name)}_slot`)
});
if (!(<InlineComponentWrapper>owner).slots.has(name)) {
const child_block = block.child({
comment: createDebuggingComment(node, this.renderer.component),
name: this.renderer.component.getUniqueName(`create_${sanitize(name)}_slot`)
});
const fn = `({ thing }) => ({ thing })`;
(<InlineComponentWrapper>owner).slots.set(name, this.slot_block);
this.renderer.blocks.push(this.slot_block);
(<InlineComponentWrapper>owner).slots.set(name, {
block: child_block,
fn
});
this.renderer.blocks.push(child_block);
}
this.slot_block = (<InlineComponentWrapper>owner).slots.get(name).block;
block = this.slot_block;
}
}

@ -13,10 +13,11 @@ import getObject from '../../../../utils/getObject';
import flattenReference from '../../../../utils/flattenReference';
import createDebuggingComment from '../../../../utils/createDebuggingComment';
import sanitize from '../../../../utils/sanitize';
import { get_context_merger } from '../shared/get_context_merger';
export default class InlineComponentWrapper extends Wrapper {
var: string;
slots: Map<string, Block> = new Map();
slots: Map<string, { block: Block, fn?: string }> = new Map();
node: InlineComponent;
fragment: FragmentWrapper;
@ -71,7 +72,13 @@ export default class InlineComponentWrapper extends Wrapper {
name: renderer.component.getUniqueName(`create_default_slot`)
});
this.renderer.blocks.push(default_slot);
this.slots.set('default', default_slot);
const fn = get_context_merger(this.node.lets);
this.slots.set('default', {
block: default_slot,
fn
});
this.fragment = new FragmentWrapper(renderer, default_slot, node.children, this, stripWhitespace, nextSibling);
block.addDependencies(default_slot.dependencies);
@ -101,7 +108,7 @@ export default class InlineComponentWrapper extends Wrapper {
const usesSpread = !!this.node.attributes.find(a => a.isSpread);
const slot_props = Array.from(this.slots).map(([name, block]) => `$$slot_${sanitize(name)}: ${block.name}`);
const slot_props = Array.from(this.slots).map(([name, slot]) => `$$slot_${sanitize(name)}: [${slot.block.name}${slot.fn ? `, ${slot.fn}` : ''}]`);
if (slot_props.length > 0) slot_props.push(`$$scope: { ctx }`);
const attributeObject = usesSpread
@ -123,7 +130,7 @@ export default class InlineComponentWrapper extends Wrapper {
const default_slot = this.slots.get('default');
this.fragment.nodes.forEach((child: Wrapper) => {
child.render(default_slot, null, 'nodes');
child.render(default_slot.block, null, 'nodes');
});
}
@ -136,8 +143,8 @@ export default class InlineComponentWrapper extends Wrapper {
}
const fragment_dependencies = new Set();
this.slots.forEach(block => {
block.dependencies.forEach(name => {
this.slots.forEach(slot => {
slot.block.dependencies.forEach(name => {
if (renderer.component.mutable_props.has(name)) {
fragment_dependencies.add(name);
}

@ -33,7 +33,7 @@ export default class SlotWrapper extends Wrapper {
nextSibling
);
block.addDependencies(new Set('$$scope'));
block.addDependencies(new Set(['$$scope']));
}
render(
@ -49,7 +49,7 @@ export default class SlotWrapper extends Wrapper {
const slot = block.getUniqueName(`${sanitize(slot_name)}_slot`);
block.builders.init.addLine(
`const ${slot} = ctx.$$slot_${sanitize(slot_name)} && ctx.$$slot_${sanitize(slot_name)}(ctx.$$scope.ctx);`
`const ${slot} = @create_slot(ctx.$$slot_${sanitize(slot_name)}, ctx);`
);
let mountBefore = block.builders.mount.toString();

@ -0,0 +1,7 @@
import Let from '../../../nodes/Let';
export function get_context_merger(lets: Let[]) {
if (lets.length === 0) return null;
return `({ ${lets.map(l => l.name).join(', ')} }) => ({ ${lets.map(l => l.name).join(', ')} })`;
}

@ -2,8 +2,9 @@ import { quotePropIfNecessary, quoteNameIfNecessary } from '../../../utils/quote
import isVoidElementName from '../../../utils/isVoidElementName';
import Attribute from '../../nodes/Attribute';
import Node from '../../nodes/shared/Node';
import { escape, escapeTemplate } from '../../../utils/stringify';
import { snip } from '../utils';
import { stringify_attribute } from './shared/stringify_attribute';
import { get_slot_context } from './shared/get_slot_context';
// source: https://gist.github.com/ArjanSchouten/0b8574a6ad7f5065a5e7
const boolean_attributes = new Set([
@ -57,6 +58,8 @@ export default function(node, renderer, options) {
const target = renderer.targets[renderer.targets.length - 1];
target.slotStack.push(slotName);
target.slots[slotName] = '';
options.slot_contexts.set(slotName, get_slot_context(node.lets));
}
const classExpr = node.classes.map((classDir: Class) => {
@ -75,7 +78,7 @@ export default function(node, renderer, options) {
args.push(snip(attribute.expression));
} else {
if (attribute.name === 'value' && node.name === 'textarea') {
textareaContents = stringifyAttribute(attribute);
textareaContents = stringify_attribute(attribute);
} else if (attribute.isTrue) {
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: true }`);
} else if (
@ -86,7 +89,7 @@ export default function(node, renderer, options) {
// a boolean attribute with one non-Text chunk
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: ${snip(attribute.chunks[0])} }`);
} else {
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: \`${stringifyAttribute(attribute)}\` }`);
args.push(`{ ${quoteNameIfNecessary(attribute.name)}: \`${stringify_attribute(attribute)}\` }`);
}
}
});
@ -97,7 +100,7 @@ export default function(node, renderer, options) {
if (attribute.type !== 'Attribute') return;
if (attribute.name === 'value' && node.name === 'textarea') {
textareaContents = stringifyAttribute(attribute);
textareaContents = stringify_attribute(attribute);
} else if (attribute.isTrue) {
openingTag += ` ${attribute.name}`;
} else if (
@ -109,14 +112,14 @@ export default function(node, renderer, options) {
openingTag += '${' + snip(attribute.chunks[0]) + ' ? " ' + attribute.name + '" : "" }';
} else if (attribute.name === 'class' && classExpr) {
addClassAttribute = false;
openingTag += ` class="\${[\`${stringifyAttribute(attribute)}\`, ${classExpr}].join(' ').trim() }"`;
openingTag += ` class="\${[\`${stringify_attribute(attribute)}\`, ${classExpr}].join(' ').trim() }"`;
} else if (attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text') {
const { name } = attribute;
const snippet = snip(attribute.chunks[0]);
openingTag += '${(v => v == null ? "" : ` ' + name + '="${@escape(' + snippet + ')}"`)(' + snippet + ')}';
} else {
openingTag += ` ${attribute.name}="${stringifyAttribute(attribute)}"`;
openingTag += ` ${attribute.name}="${stringify_attribute(attribute)}"`;
}
});
}
@ -150,15 +153,3 @@ export default function(node, renderer, options) {
renderer.append(`</${node.name}>`);
}
}
function stringifyAttribute(attribute: Attribute) {
return attribute.chunks
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return escapeTemplate(escape(chunk.data).replace(/"/g, '&quot;'));
}
return '${@escape(' + snip(chunk) + ')}';
})
.join('');
}

@ -1,11 +1,9 @@
import { escape, escapeTemplate, stringify } from '../../../utils/stringify';
import getObject from '../../../utils/getObject';
import { get_tail_snippet } from '../../../utils/get_tail_snippet';
import { quoteNameIfNecessary, quotePropIfNecessary } from '../../../utils/quoteIfNecessary';
import deindent from '../../../utils/deindent';
import { quoteNameIfNecessary } from '../../../utils/quoteIfNecessary';
import { snip } from '../utils';
import Renderer from '../Renderer';
import stringifyProps from '../../../utils/stringifyProps';
import { get_slot_context } from './shared/get_slot_context';
type AppendTarget = any; // TODO
@ -33,12 +31,6 @@ function getAttributeValue(attribute) {
return '`' + attribute.chunks.map(stringifyAttribute).join('') + '`';
}
function stringifyObject(props) {
return props.length > 0
? `{ ${props.join(', ')} }`
: `{};`
}
export default function(node, renderer: Renderer, options) {
const binding_props = [];
const binding_fns = [];
@ -98,11 +90,18 @@ export default function(node, renderer: Renderer, options) {
renderer.targets.push(target);
renderer.render(node.children, options);
const slot_contexts = new Map();
slot_contexts.set('default', get_slot_context(node.lets));
renderer.render(node.children, Object.assign({}, options, {
slot_contexts
}));
Object.keys(target.slots).forEach(name => {
const slot_context = slot_contexts.get(name);
slot_fns.push(
`${quoteNameIfNecessary(name)}: () => \`${target.slots[name]}\``
`${quoteNameIfNecessary(name)}: (${slot_context}) => \`${target.slots[name]}\``
);
});

@ -1,4 +1,6 @@
import { quotePropIfNecessary } from '../../../utils/quoteIfNecessary';
import { snip } from '../utils';
import { stringify_attribute } from './shared/stringify_attribute';
export default function(node, renderer, options) {
const name = node.attributes.find(attribute => attribute.name === 'name');
@ -6,7 +8,23 @@ export default function(node, renderer, options) {
const slot_name = name && name.chunks[0].data || 'default';
const prop = quotePropIfNecessary(slot_name);
renderer.append(`\${$$slots${prop} ? $$slots${prop}() : \``);
const slot_data = node.attributes
.filter(attribute => attribute.name !== 'name')
.map(attribute => {
const value = attribute.isTrue
? 'true'
: attribute.chunks.length === 0
? '""'
: attribute.chunks.length === 1 && attribute.chunks[0].type !== 'Text'
? snip(attribute.chunks[0])
: stringify_attribute(attribute);
return `${attribute.name}: ${value}`;
});
const arg = slot_data ? `{ ${slot_data.join(', ')} }` : '';
renderer.append(`\${$$slots${prop} ? $$slots${prop}(${arg}) : \``);
renderer.render(node.children, options);

@ -0,0 +1,6 @@
import Let from '../../../nodes/Let';
export function get_slot_context(lets: Let[]) {
if (lets.length === 0) return '';
return `{ ${lets.map(l => `${l.name}: ${l.name}`)} }`; // TODO support aliased/destructured lets
}

@ -0,0 +1,16 @@
import Attribute from '../../../nodes/Attribute';
import Node from '../../../nodes/shared/Node';
import { escapeTemplate, escape } from '../../../../utils/stringify';
import { snip } from '../../utils';
export function stringify_attribute(attribute: Attribute) {
return attribute.chunks
.map((chunk: Node) => {
if (chunk.type === 'Text') {
return escapeTemplate(escape(chunk.data).replace(/"/g, '&quot;'));
}
return '${@escape(' + snip(chunk) + ')}';
})
.join('');
}

@ -57,3 +57,13 @@ export function validate_store(store, name) {
throw new Error(`'${name}' is not a store with a 'subscribe' method`);
}
}
export function create_slot(definition, ctx) {
if (definition) {
const slot_ctx = definition[1]
? assign({}, assign(ctx.$$scope.ctx, definition[1](ctx)))
: ctx.$$scope.ctx;
return definition[0](slot_ctx);
}
}

@ -182,18 +182,19 @@ 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({
code: `invalid-slot-placement`,
message: `<slot> cannot be a child of an each-block`
}, start);
}
}
}
// TODO should this still error in in web component mode?
// if (name === 'slot') {
// let i = parser.stack.length;
// while (i--) {
// const item = parser.stack[i];
// if (item.type === 'EachBlock') {
// parser.error({
// code: `invalid-slot-placement`,
// message: `<slot> cannot be a child of an each-block`
// }, start);
// }
// }
// }
const uniqueNames = new Set();
@ -464,6 +465,7 @@ function get_directive_type(name) {
if (name === 'bind') return 'Binding';
if (name === 'class') return 'Class';
if (name === 'on') return 'EventHandler';
if (name === 'let') return 'Let';
if (name === 'ref') return 'Ref';
if (name === 'in' || name === 'out' || name === 'transition') return 'Transition';
}

@ -0,0 +1,5 @@
<div>
{#each things as thing}
<slot name="foo" {thing}/>
{/each}
</div>

@ -0,0 +1,24 @@
export default {
props: {
things: [1, 2, 3]
},
html: `
<div>
<div slot="foo"><span>1</span></div>
<div slot="foo"><span>2</span></div>
<div slot="foo"><span>3</span></div>
</div>`,
test({ assert, component, target }) {
component.things = [1, 2, 3, 4];
assert.htmlEqual(target.innerHTML, `
<div>
<div slot="foo"><span>1</span></div>
<div slot="foo"><span>2</span></div>
<div slot="foo"><span>3</span></div>
<div slot="foo"><span>4</span></div>
</div>
`);
}
};

@ -0,0 +1,11 @@
<script>
import Nested from './Nested.html';
export let things;
</script>
<Nested {things}>
<div slot="foo" let:thing>
<span>{thing}</span>
</div>
</Nested>

@ -0,0 +1,5 @@
<div>
{#each things as thing}
<slot {thing}/>
{/each}
</div>

@ -0,0 +1,24 @@
export default {
props: {
things: [1, 2, 3]
},
html: `
<div>
<span>1</span>
<span>2</span>
<span>3</span>
</div>`,
test({ assert, component, target }) {
component.things = [1, 2, 3, 4];
assert.htmlEqual(target.innerHTML, `
<div>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
</div>
`);
}
};

@ -0,0 +1,9 @@
<script>
import Nested from './Nested.html';
export let things;
</script>
<Nested {things} let:thing>
<span>{thing}</span>
</Nested>
Loading…
Cancel
Save