Skip to content

Commit

Permalink
feat: add named slots (#161)
Browse files Browse the repository at this point in the history
  • Loading branch information
prevwong committed Jun 17, 2024
1 parent 4502a45 commit c82817e
Show file tree
Hide file tree
Showing 12 changed files with 245 additions and 73 deletions.
7 changes: 7 additions & 0 deletions .changeset/witty-students-decide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@rekajs/parser': patch
'@rekajs/types': patch
'@rekajs/core': patch
---

Add named slots
22 changes: 20 additions & 2 deletions packages/core/src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,16 +196,34 @@ 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],
owner: this.ctx.owner,
})
);

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,
});

Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
Expand Down
6 changes: 5 additions & 1 deletion packages/parser/src/lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}

Expand Down
104 changes: 67 additions & 37 deletions packages/parser/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,8 @@ const jsToReka = <T extends t.ASTNode = t.ASTNode>(
if: null,
each: null,
classList: null,
name: null,
accepsts: null,
};

const props = node.openingElement.attributes.reduce((accum, attr) => {
Expand Down Expand Up @@ -608,18 +610,36 @@ class _Parser extends Lexer {
});
}

private parseElementContent() {
private parseElementContent(parent?: t.SlottableTemplate) {
const children: t.Template[] = [];
const props: Record<string, any> = {};

const slotChildren: Record<string, t.Template[]> = {};
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) &&
Expand All @@ -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 {
Expand All @@ -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;
}
}
}

Expand All @@ -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();
Expand All @@ -678,7 +711,7 @@ class _Parser extends Lexer {
break contents;
}

children.push(this.parseElementContent());
this.parseElementContent(tpl);
break;
}
case TokenType.ELEMENT_EXPR_START: {
Expand All @@ -689,7 +722,7 @@ class _Parser extends Lexer {
`Expected literal value as text value`
);

children.push(
tpl.children.push(
t.tagTemplate({
tag: 'text',
props: {
Expand All @@ -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<T extends t.Type>(opts?: AcornParserOptions<T>) {
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<T extends t.Type = t.Any>(
Expand Down
63 changes: 59 additions & 4 deletions packages/parser/src/stringifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any> = {}
) {
const value = this.opts.onStringifyNode(node);

if (value) {
Expand Down Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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');
}
Expand Down
Loading

0 comments on commit c82817e

Please sign in to comment.