web components (custom-element) support: Conversion from .astro component to createComponent break when an anchor tag is introduced
schalkneethling opened this issue · 2 comments
Astro Info
I ran into an interesting bug (I believe it to be one) when using web components with Astro. After reducing my test case I can narrow it down to be related to custom elements.
Astro v5.0.3
Node v22.8.0
System macOS (arm64)
Package Manager npm
Output static
Adapter none
Integrations none
If this issue only occurs in one browser, which browser is a problem?
No response
Describe the Bug
After reducing the test case to the following:
<ul class="card-list">
{
users.length &&
users.map((user: User) => (
<li>
<article class="user-card">
<h2 class="user-card-title">
{user.firstName} {user.lastName}
</h2>
<img
class="user-card-avatar"
src={user.avatarURL}
height="150"
width="150"
alt=""
/>
<span class="user-card-role">{user.role}</span>
<span class="user-card-email">
<span class="visually-hidden">Email:</span>
<a href={`mailto:${user.email}`}>{user.email}</a>
</span>
</article>
</li>
))
}
</ul>
The build works without any trouble. When I wrap this in a template
element as follows, everything is still A-OK:
<template shadowrootmode="open">
<style set:html={style}></style>
<ul class="card-list">
{
users.length &&
users.map((user: User) => (
<li>
// same as above - omitting for brevity
</li>
))
}
</ul>
</template>
If I know add my wrapper custom element:
<nimbus-team>
<template shadowrootmode="open">
<style set:html={style}></style>
<ul class="card-list">
{
users.length &&
users.map((user: User) => (
<li>
// same as above - omitting for brevity
</li>
))
}
</ul>
</template>
</nimbus-team>
The build fails with:
Stack Trace
20:02:31 ▶ src/pages/index.astro
20:02:31 └─ /index.htmlfile:///Users/schalkneethling/dev/opensource/neo-shadow/dist/pages/index.astro.mjs?time=1733680951649:18
return renderTemplate`${renderComponent($$result, "nimbus-team", "nimbus-team", {}, { "default": () => renderTemplate` <template shadowrootmode="open"> <style>${unescapeHTML(style)}</style> ${maybeRenderHead()}<ul class="card-list"> ${users.length && users.map((user2) => renderTemplate`<li> <article class="user-card"> <h2 class="user-card-title"> ${user2.firstName} ${user2.lastName} </h2> <img class="user-card-avatar"${addAttribute(user2.avatarURL, "src")} height="150" width="150" alt=""> <span class="user-card-role">${user2.role}</span> <span class="user-card-email"> <span class="visually-hidden">Email:</span> <a${addAttribute(`mailto:${user2.email}`, "href")}>${user2.email}</a> </span> </article> </li>`)} </ul> </template><a${addAttribute(`mailto:${user.email}`, "href")}></a>` })}<a${addAttribute(`mailto:${user.email}`, "href")}> ${renderScript($$result, "/Users/schalkneethling/dev/opensource/neo-shadow/src/components/NimbusTeam.astro?astro&type=script&index=0&lang.ts")}</a>`;
^
ReferenceError: user is not defined
at default (file:///Users/schalkneethling/dev/opensource/neo-shadow/dist/pages/index.astro.mjs?time=1733680951649:18:764)
at Object.render (file:///Users/schalkneethling/dev/opensource/neo-shadow/dist/chunks/astro/server_57EaB_XO.mjs:788:70)
at renderSlotToString (file:///Users/schalkneethling/dev/opensource/neo-shadow/dist/chunks/astro/server_57EaB_XO.mjs:815:24)
at file:///Users/schalkneethling/dev/opensource/neo-shadow/dist/chunks/astro/server_57EaB_XO.mjs:824:27
at Array.map (<anonymous>)
at renderSlots (file:///Users/schalkneethling/dev/opensource/neo-shadow/dist/chunks/astro/server_57EaB_XO.mjs:823:29)
at renderFrameworkComponent (file:///Users/schalkneethling/dev/opensource/neo-shadow/dist/chunks/astro/server_57EaB_XO.mjs:1228:48)
at renderComponent (file:///Users/schalkneethling/dev/opensource/neo-shadow/dist/chunks/astro/server_57EaB_XO.mjs:1512:16)
at file:///Users/schalkneethling/dev/opensource/neo-shadow/dist/pages/index.astro.mjs?time=1733680951649:18:27
at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
Node.js v22.8.0
After referring to the generated intermediary dist/pages/index.astro.mjs
one can see that the conversion from the Astro component to JS is going awry:
const $$NimbusTeam = createComponent(async ($$result, $$props, $$slots) => {
const response = await fetch(
"https://fictionalfolks.netlify.app/.netlify/functions/users?count=2"
);
let users;
if (response.ok) {
users = await response.json();
}
return renderTemplate`${renderComponent($$result, "nimbus-team", "nimbus-team", {}, { "default": () => renderTemplate` <template shadowrootmode="open"> <style>${unescapeHTML(style)}</style> ${maybeRenderHead()}<ul class="card-list"> ${users.length && users.map((user2) => renderTemplate`<li> <article class="user-card"> <h2 class="user-card-title"> ${user2.firstName} ${user2.lastName} </h2> <img class="user-card-avatar"${addAttribute(user2.avatarURL, "src")} height="150" width="150" alt=""> <span class="user-card-role">${user2.role}</span> <span class="user-card-email"> <span class="visually-hidden">Email:</span> <a${addAttribute(`mailto:${user2.email}`, "href")}>${user2.email}</a> </span> </article> </li>`)} </ul> </template><a${addAttribute(`mailto:${user.email}`, "href")}></a>` })}<a${addAttribute(`mailto:${user.email}`, "href")}> ${renderScript($$result, "/Users/schalkneethling/dev/opensource/neo-shadow/src/components/NimbusTeam.astro?astro&type=script&index=0&lang.ts")}</a>`;
}, "/Users/schalkneethling/dev/opensource/neo-shadow/src/components/NimbusTeam.astro", void 0);
A few standout items for me:
return renderTemplate`${renderComponent($$result, "nimbus-team", "nimbus-team", {}, { "default": () =>
nimbus-team
is repeated twice in the signature. Having not looked at the signature, it could be that this is as expected, but it reads a little curious to me.
${users.length && users.map((user2) => renderTemplate
The use of user2
here instead of user
as defined in the Astro component seems odd and is the cause of the user is undefined
error a bit later in this snippet.
<a${addAttribute(`mailto:${user2.email}`, "href")}>${user2.email}</a> </span> </article> </li>`)} </ul> </template>
The renderTemplate
function call seems to end abruptly after the last closing list item even though it starts from <template shadowrootmode="open">
.
The anchor element <a${addAttribute(
mailto:${user2.email}, "href")}>${user2.email}</a>
from a little earlier in the snippet is replicated twice more and in fact, the last of these wraps the script
element.
<a${addAttribute(`mailto:${user.email}`, "href")}></a>` })}<a${addAttribute(`mailto:${user.email}`, "href")}> ${renderScript($$result, "/Users/schalkneethling/dev/opensource/neo-shadow/src/components/NimbusTeam.astro?astro&type=script&index=0&lang.ts")}</a>
What is also curious here is that:
- These are outside of the
template
_and - the content
${user2.email}
of the anchor element is removed.
This is truly peculiar and I would be happy to assist further in tracking down and fixing whatever is causing this. You can see the full Astro component at the following URL:
https://github.com/schalkneethling/neo-shadow/blob/main/src/components/NimbusTeam.astro
What's the expected result?
The build succeeds and produces the expected HTML.
Link to Minimal Reproducible Example
https://github.com/schalkneethling/neo-shadow/blob/main/src/components/NimbusTeam.astro
Participation
- I am willing to submit a pull request for this issue.
BTW, if I do not use declarative ShadowDOM and do not nest the custom element and template
element I still get the error, but the stack trace is less detailed:
Astro
<nimbus-team></nimbus-team>
<template>
<style set:html={style}></style>
<ul class="card-list">
{
users.length &&
users.map((user: User) => (
<li>
<article class="user-card">
<h2 class="user-card-title">
{user.firstName} {user.lastName}
</h2>
<img
class="user-card-avatar"
src={user.avatarURL}
height="150"
width="150"
alt=""
/>
<span class="user-card-role">{user.role}</span>
<span class="user-card-email">
<span class="visually-hidden">Email:</span>
<a href={`mailto:${user.email}`}>{user.email}</a>
</span>
</article>
</li>
))
}
</ul>
</template>
Stack Trace
20:18:09 ▶ src/pages/index.astro
20:18:09 └─ /index.htmluser is not defined
Stack trace:
at file:///Users/schalkneethling/dev/opensource/neo-shadow/dist/pages/index.astro.mjs?time=1733681889426:18:708
createComponent for the $$NimbusTeam
Astro component
const $$NimbusTeam = createComponent(async ($$result, $$props, $$slots) => {
const response = await fetch(
"https://fictionalfolks.netlify.app/.netlify/functions/users?count=2"
);
let users;
if (response.ok) {
users = await response.json();
}
return renderTemplate`${renderComponent($$result, "nimbus-team", "nimbus-team", {})} <template> <style>${unescapeHTML(style)}</style> ${maybeRenderHead()}<ul class="card-list"> ${users.length && users.map((user2) => renderTemplate`<li> <article class="user-card"> <h2 class="user-card-title"> ${user2.firstName} ${user2.lastName} </h2> <img class="user-card-avatar"${addAttribute(user2.avatarURL, "src")} height="150" width="150" alt=""> <span class="user-card-role">${user2.role}</span> <span class="user-card-email"> <span class="visually-hidden">Email:</span> <a${addAttribute(`mailto:${user2.email}`, "href")}>${user2.email}</a> </span> </article> </li>`)} </ul> </template><a${addAttribute(`mailto:${user.email}`, "href")}> ${renderScript($$result, "/Users/schalkneethling/dev/opensource/neo-shadow/src/components/NimbusTeam.astro?astro&type=script&index=0&lang.ts")}</a>`;
}, "/Users/schalkneethling/dev/opensource/neo-shadow/src/components/NimbusTeam.astro", void 0);
UPDATE: I was able to "fix" this by changing the data structure and looping over the social platforms to create the list items. I have "fix" in quotations because I am still unsure why having the list items hard-coded inside the template
inside the custom element trips up the (I think) compiler so badly. It is most likely still worth investigating (happy to help).
Here is the part that changed:
<ul class="user-card-social">
{Object.entries(user.social).map(([key, value]) => (
<li>
<a
class={`icon icon-social-${key}`}
href={value.url}
target="_blank"
rel="noopener noreferrer"
>
<span class="visually-hidden">
Follow {user.firstName} on {value.name}
</span>
</a>
</li>
))}
</ul>
The entire component can be reviewed here and the commit with the change(s) can be reviewed here.