Add function-based slot support to Astro.slots.render() (#2954)

* feat(slots): add function-based slot support to Astro.slots.render()

* test(slots): add render tests
This commit is contained in:
Nate Moore 2022-03-31 13:11:26 -05:00 committed by GitHub
parent 50af480c7d
commit d81b6d9ebc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 94 additions and 5 deletions

View file

@ -0,0 +1,7 @@
---
'astro': patch
---
Improve `Astro.slots` API to support passing arguments to function-based slots.
This allows for more ergonomic utility components that accept a callback function as a child.

View file

@ -60,7 +60,7 @@ export interface AstroGlobal extends AstroGlobalPartial {
/** get information about this page */
request: Request;
/** see if slots are used */
slots: Record<string, true | undefined> & { has(slotName: string): boolean; render(slotName: string): Promise<string> };
slots: Record<string, true | undefined> & { has(slotName: string): boolean; render(slotName: string, args?: any[]): Promise<string> };
}
export interface AstroGlobalPartial {

View file

@ -28,6 +28,12 @@ export interface CreateResultArgs {
request: Request;
}
function getFunctionExpression(slot: any) {
if (!slot) return;
if (slot.expressions?.length !== 1) return;
return slot.expressions[0] as (...args: any[]) => any;
}
class Slots {
#cache = new Map<string, string>();
#result: SSRResult;
@ -56,15 +62,24 @@ class Slots {
return Boolean(this.#slots[name]);
}
public async render(name: string) {
public async render(name: string, args: any[] = []) {
const cacheable = args.length === 0;
if (!this.#slots) return undefined;
if (this.#cache.has(name)) {
if (cacheable && this.#cache.has(name)) {
const result = this.#cache.get(name);
return result;
}
if (!this.has(name)) return undefined;
const content = await renderSlot(this.#result, this.#slots[name]).then((res) => (res != null ? res.toString() : res));
this.#cache.set(name, content);
if (!cacheable) {
const component = await this.#slots[name]();
const expression = getFunctionExpression(component);
if (expression) {
const slot = expression(...args);
return await renderSlot(this.#result, slot).then((res) => res != null ? String(res) : res);
}
}
const content = await renderSlot(this.#result, this.#slots[name]).then((res) => res != null ? String(res) : res);
if (cacheable) this.#cache.set(name, content);
return content;
}
}

View file

@ -112,4 +112,33 @@ describe('Slots', () => {
expect($('#default')).to.have.lengthOf(1); // the default slot is filled
}
});
it('Slots.render() API', async () => {
// Simple imperative slot render
{
const html = await fixture.readFile('/slottedapi-render/index.html');
const $ = cheerio.load(html);
expect($('#render')).to.have.lengthOf(1);
expect($('#render').text()).to.equal('render');
}
// Child function render without args
{
const html = await fixture.readFile('/slottedapi-render/index.html');
const $ = cheerio.load(html);
expect($('#render-fn')).to.have.lengthOf(1);
expect($('#render-fn').text()).to.equal('render-fn');
}
// Child function render with args
{
const html = await fixture.readFile('/slottedapi-render/index.html');
const $ = cheerio.load(html);
expect($('#render-args')).to.have.lengthOf(1);
expect($('#render-args').text()).to.equal('render-args');
}
});
});

View file

@ -0,0 +1,6 @@
---
const { id } = Astro.props;
const content = await Astro.slots.render('default');
---
<div id={id} set:html={content} />

View file

@ -0,0 +1,6 @@
---
const { id, text } = Astro.props;
const content = await Astro.slots.render('default', [text]);
---
<div id={id} set:html={content} />

View file

@ -0,0 +1,6 @@
---
const { id } = Astro.props;
const content = await Astro.slots.render('default');
---
<div id={id} set:html={content} />

View file

@ -0,0 +1,20 @@
---
import Render from '../components/Render.astro';
import RenderFn from '../components/RenderFn.astro';
import RenderArgs from '../components/RenderArgs.astro';
---
<html>
<head>
<!--
Test Astro.slots.render behavior.
- `Render` is basic imperative `render` call
- `RenderFn` is `render` that calls child function with arguments
-->
</head>
<body>
<Render id="render">render</Render>
<RenderFn id="render-fn">{() => "render-fn"}</RenderFn>
<RenderArgs id="render-args" text="render-args">{(text: string) => text}</RenderArgs>
</body>
</html>