diff --git a/.changeset/witty-students-decide.md b/.changeset/witty-students-decide.md new file mode 100644 index 00000000..35aa405e --- /dev/null +++ b/.changeset/witty-students-decide.md @@ -0,0 +1,7 @@ +--- +'@rekajs/parser': patch +'@rekajs/types': patch +'@rekajs/core': patch +--- + +Add named slots diff --git a/packages/core/src/component.ts b/packages/core/src/component.ts index 886af3a2..65841434 100644 --- a/packages/core/src/component.ts +++ b/packages/core/src/component.ts @@ -196,7 +196,7 @@ export class ComponentViewEvaluator { if (!this.rekaComponentPropsComputation) { this.rekaComponentPropsComputation = computed( () => { - const slot = this.template.children.flatMap((child) => + const children = this.template.children.flatMap((child) => this.evaluator.computeTemplate(child, { ...this.ctx, path: [...this.ctx.path, child.id], @@ -204,8 +204,26 @@ export class ComponentViewEvaluator { }) ); + const namedSlots = Object.keys(this.template.slots).reduce( + (accum, name) => { + accum[name] = this.template.slots[name].flatMap((child) => + this.evaluator.computeTemplate(child, { + ...this.ctx, + path: [...this.ctx.path, child.id], + owner: this.ctx.owner, + }) + ); + + return accum; + }, + {} + ); + this.env.set(ComponentSlotBindingKey, { - value: slot, + value: { + children, + ...namedSlots, + }, readonly: true, }); diff --git a/packages/core/src/evaluator.ts b/packages/core/src/evaluator.ts index 92303aa4..4260ded6 100644 --- a/packages/core/src/evaluator.ts +++ b/packages/core/src/evaluator.ts @@ -455,11 +455,15 @@ export class Evaluator { } computeSlotTemplate(template: t.SlotTemplate, ctx: TemplateEvaluateContext) { + const slotValue = ctx.env.getByName(ComponentSlotBindingKey)[ + template.name ? template.name : 'children' + ]; + return [ t.slotView({ key: createKey(ctx.path), template, - children: ctx.env.getByName(ComponentSlotBindingKey), + children: slotValue ?? [], frame: this.frame.id, owner: ctx.owner, }), diff --git a/packages/parser/src/lexer.ts b/packages/parser/src/lexer.ts index b8cf716c..b7273cb9 100644 --- a/packages/parser/src/lexer.ts +++ b/packages/parser/src/lexer.ts @@ -182,7 +182,11 @@ export class Lexer { this.advanceCharWhile((c) => this.isAlpha(c)); const word = this.readWord(); - if (['if', 'each', 'classList'].includes(word) === false) { + if ( + ['if', 'each', 'classList', 'name', 'accepts', 'slot'].includes( + word + ) === false + ) { throw new Error(`Unknown element directive: ${word}`); } diff --git a/packages/parser/src/parser.ts b/packages/parser/src/parser.ts index 0b73dd56..4fce16f1 100644 --- a/packages/parser/src/parser.ts +++ b/packages/parser/src/parser.ts @@ -219,6 +219,8 @@ const jsToReka = ( if: null, each: null, classList: null, + name: null, + accepsts: null, }; const props = node.openingElement.attributes.reduce((accum, attr) => { @@ -608,18 +610,36 @@ class _Parser extends Lexer { }); } - private parseElementContent() { + private parseElementContent(parent?: t.SlottableTemplate) { const children: t.Template[] = []; - const props: Record = {}; - + const slotChildren: Record = {}; let closingTag: string | null = null; const tag = this.consume(TokenType.ELEMENT_PROPERTY).value; - const directives = { - each: undefined, - if: undefined, - }; + let tpl: t.Template; + + const isComponent = tag[0] === tag[0].toUpperCase(); + + if (isComponent) { + tpl = t.componentTemplate({ + component: getIdentifierFromStr(tag), + children, + slots: slotChildren, + }); + } else if (tag === 'slot') { + tpl = t.slotTemplate({ + props: {}, + }); + } else { + tpl = t.tagTemplate({ + tag, + children, + slots: slotChildren, + }); + } + + let slotEntryName = null; while ( !this.check(TokenType.ELEMENT_TAG_END) && @@ -632,7 +652,7 @@ class _Parser extends Lexer { if (this.match(TokenType.COLON)) { this.consume(TokenType.EQ); - props[propName] = t.propBinding({ + tpl.props[propName] = t.propBinding({ identifier: t.assert(this.parseElementExpr(), t.Identifier), }); } else { @@ -648,17 +668,28 @@ class _Parser extends Lexer { propValue = this.parseElementExpr(); } - props[propName] = propValue; + tpl.props[propName] = propValue; } } else { const directive = this.consume(TokenType.ELEMENT_DIRECTIVE).value; + if (directive === 'accepts') { + invariant( + t.is(tpl, t.SlotTemplate), + `The "@accepts" directive can only be used with SlotTemplate type` + ); + } this.consume(TokenType.EQ); + const directiveValue = directive === 'each' ? this.parseElementEach() : this.parseElementExpr(); - directives[directive] = directiveValue; + if (directive === 'slot') { + slotEntryName = directiveValue; + } else { + tpl[directive] = directiveValue; + } } } @@ -668,6 +699,8 @@ class _Parser extends Lexer { if (!selfClosing) { contents: for (;;) { + invariant(tpl && t.is(tpl, t.SlottableTemplate)); + switch (this.currentToken.type) { case TokenType.ELEMENT_TAG_START: { this.next(); @@ -678,7 +711,7 @@ class _Parser extends Lexer { break contents; } - children.push(this.parseElementContent()); + this.parseElementContent(tpl); break; } case TokenType.ELEMENT_EXPR_START: { @@ -689,7 +722,7 @@ class _Parser extends Lexer { `Expected literal value as text value` ); - children.push( + tpl.children.push( t.tagTemplate({ tag: 'text', props: { @@ -714,36 +747,33 @@ class _Parser extends Lexer { } } - const isComponent = tag[0] === tag[0].toUpperCase(); - - if (isComponent) { - return t.componentTemplate({ - component: getIdentifierFromStr(tag), - props, - children, - ...directives, - }); - } - - if (tag === 'slot') { - return t.slotTemplate({ - props: {}, - }); + if (parent) { + if (!slotEntryName) { + parent.children.push(tpl); + } else { + parent.slots[slotEntryName] = [ + ...(parent.slots[slotEntryName] || []), + tpl, + ]; + } } - return t.tagTemplate({ - tag, - props, - children, - ...directives, - }); + return tpl; } private parseElementExpr(opts?: AcornParserOptions) { - this.consume(TokenType.ELEMENT_EXPR_START); - const expr = this.parseExpressionAt(this.previousToken.pos + 1, opts); - this.consume(TokenType.ELEMENT_EXPR_END); - return expr; + if (this.check(TokenType.STRING)) { + return this.consume(TokenType.STRING).value; + } + + if (this.check(TokenType.ELEMENT_EXPR_START)) { + this.consume(TokenType.ELEMENT_EXPR_START); + const expr = this.parseExpressionAt(this.previousToken.pos + 1, opts); + this.consume(TokenType.ELEMENT_EXPR_END); + return expr; + } + + return this.currentToken.value; } private parseExpressionAt( diff --git a/packages/parser/src/stringifier.ts b/packages/parser/src/stringifier.ts index f5b07776..12204428 100644 --- a/packages/parser/src/stringifier.ts +++ b/packages/parser/src/stringifier.ts @@ -97,7 +97,11 @@ class _Stringifier { this.writer.write(result); } - stringify(node: t.ASTNode, precedence: Precedence = Precedence.Sequence) { + stringify( + node: t.ASTNode, + precedence: Precedence = Precedence.Sequence, + context: Record = {} + ) { const value = this.opts.onStringifyNode(node); if (value) { @@ -466,6 +470,38 @@ class _Stringifier { ); } + if (t.is(node, t.SlotTemplate) && node.name) { + const slotName = node.name; + + if (slotName) { + props.push( + this.writer.withTemp(() => { + this.writer.write(`@name="${slotName}"`); + }) + ); + } + + if (node.accepts) { + const componentIdentifier = node.accepts; + + props.push( + this.writer.withTemp(() => { + this.writer.write(`@accepts={`); + this.stringify(componentIdentifier); + this.writer.write('}'); + }) + ); + } + } + + if (context['slotName'] !== undefined) { + props.push( + this.writer.withTemp(() => + this.writer.write(`@slot="${context['slotName']}"`) + ) + ); + } + const flattenedProps = props.reduce( (accum, prop, i, arr) => [ ...accum, @@ -486,7 +522,10 @@ class _Stringifier { } } - if (t.is(node, t.SlottableTemplate) && node.children.length > 0) { + if ( + t.is(node, t.SlottableTemplate) && + (node.children.length > 0 || Object.keys(node.slots).length > 0) + ) { this.writer.write('>'); result.push('>'); } else { @@ -499,8 +538,24 @@ class _Stringifier { if (t.is(node, t.SlottableTemplate)) { this.writer.withIndent(() => { - node.children.forEach((child, i, arr) => { - this.stringify(child); + const children: Array<[string | null, t.Template]> = []; + + Object.entries(node.slots).forEach(([slotName, tpls]) => { + tpls.forEach((tpl) => children.push([slotName, tpl])); + }); + + node.children.map((child) => children.push([null, child])); + + children.forEach(([slotName, child], i, arr) => { + this.stringify( + child, + precedence, + slotName + ? { + slotName, + } + : {} + ); if (i !== arr.length - 1) { this.writer.write('\n'); } diff --git a/packages/types/src/generated/types.generated.ts b/packages/types/src/generated/types.generated.ts index d7904563..531f5a20 100644 --- a/packages/types/src/generated/types.generated.ts +++ b/packages/types/src/generated/types.generated.ts @@ -1,6 +1,7 @@ -import { Type, TypeConstructorOptions } from '../node'; import { Schema } from '../schema'; +import { Type, TypeConstructorOptions } from '../node'; + type StateParameters = { program: Program; extensions?: Record; @@ -797,6 +798,7 @@ type SlottableTemplateParameters = { each?: ElementEach | null; classList?: ObjectExpression | null; children?: Array