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:
parent
50af480c7d
commit
d81b6d9ebc
8 changed files with 94 additions and 5 deletions
7
.changeset/light-apricots-sort.md
Normal file
7
.changeset/light-apricots-sort.md
Normal 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.
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
6
packages/astro/test/fixtures/astro-slots/src/components/Render.astro
vendored
Normal file
6
packages/astro/test/fixtures/astro-slots/src/components/Render.astro
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
const { id } = Astro.props;
|
||||
const content = await Astro.slots.render('default');
|
||||
---
|
||||
|
||||
<div id={id} set:html={content} />
|
6
packages/astro/test/fixtures/astro-slots/src/components/RenderArgs.astro
vendored
Normal file
6
packages/astro/test/fixtures/astro-slots/src/components/RenderArgs.astro
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
const { id, text } = Astro.props;
|
||||
const content = await Astro.slots.render('default', [text]);
|
||||
---
|
||||
|
||||
<div id={id} set:html={content} />
|
6
packages/astro/test/fixtures/astro-slots/src/components/RenderFn.astro
vendored
Normal file
6
packages/astro/test/fixtures/astro-slots/src/components/RenderFn.astro
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
const { id } = Astro.props;
|
||||
const content = await Astro.slots.render('default');
|
||||
---
|
||||
|
||||
<div id={id} set:html={content} />
|
20
packages/astro/test/fixtures/astro-slots/src/pages/slottedapi-render.astro
vendored
Normal file
20
packages/astro/test/fixtures/astro-slots/src/pages/slottedapi-render.astro
vendored
Normal 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>
|
Loading…
Reference in a new issue