0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-16 21:46:22 -05:00

unnest astro-island class (#10839)

This commit is contained in:
Joe Pea 2024-04-22 11:13:15 -07:00 committed by GitHub
parent 8e6eb624ae
commit b0de82b1e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -44,170 +44,174 @@ declare const Astro: {
return Object.fromEntries(Object.entries(raw).map(([key, value]) => [key, reviveTuple(value)]));
};
if (!customElements.get('astro-island')) {
customElements.define(
'astro-island',
class extends HTMLElement {
public Component: any;
public hydrator: any;
static observedAttributes = ['props'];
disconnectedCallback() {
document.removeEventListener('astro:after-swap', this.unmount);
document.addEventListener('astro:after-swap', this.unmount, { once: true });
}
connectedCallback() {
// 🌊🏝️🌴
class AstroIsland extends HTMLElement {
public Component: any;
public hydrator: any;
static observedAttributes = ['props'];
disconnectedCallback() {
document.removeEventListener('astro:after-swap', this.unmount);
document.addEventListener('astro:after-swap', this.unmount, { once: true });
}
connectedCallback() {
if (
!this.hasAttribute('await-children') ||
document.readyState === 'interactive' ||
document.readyState === 'complete'
) {
this.childrenConnectedCallback();
} else {
// connectedCallback may run *before* children are rendered (ex. HTML streaming)
// If SSR children are expected, but not yet rendered, wait with a mutation observer
// for a special marker inserted when rendering islands that signals the end of the island
const onConnected = () => {
document.removeEventListener('DOMContentLoaded', onConnected);
mo.disconnect();
this.childrenConnectedCallback();
};
const mo = new MutationObserver(() => {
if (
!this.hasAttribute('await-children') ||
document.readyState === 'interactive' ||
document.readyState === 'complete'
this.lastChild?.nodeType === Node.COMMENT_NODE &&
this.lastChild.nodeValue === 'astro:end'
) {
this.childrenConnectedCallback();
} else {
// connectedCallback may run *before* children are rendered (ex. HTML streaming)
// If SSR children are expected, but not yet rendered, wait with a mutation observer
// for a special marker inserted when rendering islands that signals the end of the island
const onConnected = () => {
document.removeEventListener('DOMContentLoaded', onConnected);
mo.disconnect();
this.childrenConnectedCallback();
};
const mo = new MutationObserver(() => {
if (
this.lastChild?.nodeType === Node.COMMENT_NODE &&
this.lastChild.nodeValue === 'astro:end'
) {
this.lastChild.remove();
onConnected();
}
});
mo.observe(this, { childList: true });
// in case the marker comment got stripped and the mutation observer waited indefinitely,
// also wait for DOMContentLoaded as a last resort
document.addEventListener('DOMContentLoaded', onConnected);
this.lastChild.remove();
onConnected();
}
}
async childrenConnectedCallback() {
let beforeHydrationUrl = this.getAttribute('before-hydration-url');
if (beforeHydrationUrl) {
await import(beforeHydrationUrl);
}
this.start();
}
async start() {
const opts = JSON.parse(this.getAttribute('opts')!) as Record<string, any>;
const directive = this.getAttribute('client') as directiveAstroKeys;
if (Astro[directive] === undefined) {
window.addEventListener(`astro:${directive}`, () => this.start(), { once: true });
return;
}
try {
await Astro[directive]!(
async () => {
const rendererUrl = this.getAttribute('renderer-url');
const [componentModule, { default: hydrator }] = await Promise.all([
import(this.getAttribute('component-url')!),
rendererUrl ? import(rendererUrl) : () => () => {},
]);
const componentExport = this.getAttribute('component-export') || 'default';
if (!componentExport.includes('.')) {
this.Component = componentModule[componentExport];
} else {
this.Component = componentModule;
for (const part of componentExport.split('.')) {
this.Component = this.Component[part];
}
}
this.hydrator = hydrator;
return this.hydrate;
},
opts,
this
);
} catch (e) {
// eslint-disable-next-line no-console
console.error(
`[astro-island] Error hydrating ${this.getAttribute('component-url')}`,
e
);
}
}
hydrate = async () => {
// The client directive needs to load the hydrator code before it can hydrate
if (!this.hydrator) return;
// Make sure the island is mounted on the DOM before hydrating. It could be unmounted
// when the parent island hydrates and re-creates this island.
if (!this.isConnected) return;
// Wait for parent island to hydrate first so we hydrate top-down. The `ssr` attribute
// represents that it has not completed hydration yet.
const parentSsrIsland = this.parentElement?.closest('astro-island[ssr]');
if (parentSsrIsland) {
parentSsrIsland.addEventListener('astro:hydrate', this.hydrate, { once: true });
return;
}
const slotted = this.querySelectorAll('astro-slot');
const slots: Record<string, string> = {};
// Always check to see if there are templates.
// This happens if slots were passed but the client component did not render them.
const templates = this.querySelectorAll('template[data-astro-template]');
for (const template of templates) {
const closest = template.closest(this.tagName);
if (!closest?.isSameNode(this)) continue;
slots[template.getAttribute('data-astro-template') || 'default'] = template.innerHTML;
template.remove();
}
for (const slot of slotted) {
const closest = slot.closest(this.tagName);
if (!closest?.isSameNode(this)) continue;
slots[slot.getAttribute('name') || 'default'] = slot.innerHTML;
}
let props: Record<string, unknown>;
try {
props = this.hasAttribute('props')
? reviveObject(JSON.parse(this.getAttribute('props')!))
: {};
} catch (e) {
let componentName: string = this.getAttribute('component-url') || '<unknown>';
const componentExport = this.getAttribute('component-export');
if (componentExport) {
componentName += ` (export ${componentExport})`;
}
// eslint-disable-next-line no-console
console.error(
`[hydrate] Error parsing props for component ${componentName}`,
this.getAttribute('props'),
e
);
throw e;
}
let hydrationTimeStart;
const hydrator = this.hydrator(this);
if (process.env.NODE_ENV === 'development') hydrationTimeStart = performance.now();
await hydrator(this.Component, props, slots, {
client: this.getAttribute('client'),
});
if (process.env.NODE_ENV === 'development' && hydrationTimeStart)
this.setAttribute(
'client-render-time',
(performance.now() - hydrationTimeStart).toString()
);
this.removeAttribute('ssr');
this.dispatchEvent(new CustomEvent('astro:hydrate'));
};
attributeChangedCallback() {
this.hydrate();
}
unmount = () => {
// If element wasn't persisted, fire unmount event
if (!this.isConnected) this.dispatchEvent(new CustomEvent('astro:unmount'));
};
});
mo.observe(this, { childList: true });
// in case the marker comment got stripped and the mutation observer waited indefinitely,
// also wait for DOMContentLoaded as a last resort
document.addEventListener('DOMContentLoaded', onConnected);
}
);
}
async childrenConnectedCallback() {
let beforeHydrationUrl = this.getAttribute('before-hydration-url');
if (beforeHydrationUrl) {
await import(beforeHydrationUrl);
}
this.start();
}
async start() {
const opts = JSON.parse(this.getAttribute('opts')!) as Record<string, any>;
const directive = this.getAttribute('client') as directiveAstroKeys;
if (Astro[directive] === undefined) {
window.addEventListener(`astro:${directive}`, () => this.start(), { once: true });
return;
}
try {
await Astro[directive]!(
async () => {
const rendererUrl = this.getAttribute('renderer-url');
const [componentModule, { default: hydrator }] = await Promise.all([
import(this.getAttribute('component-url')!),
rendererUrl ? import(rendererUrl) : () => () => {},
]);
const componentExport = this.getAttribute('component-export') || 'default';
if (!componentExport.includes('.')) {
this.Component = componentModule[componentExport];
} else {
this.Component = componentModule;
for (const part of componentExport.split('.')) {
this.Component = this.Component[part];
}
}
this.hydrator = hydrator;
return this.hydrate;
},
opts,
this
);
} catch (e) {
// eslint-disable-next-line no-console
console.error(`[astro-island] Error hydrating ${this.getAttribute('component-url')}`, e);
}
}
hydrate = async () => {
// The client directive needs to load the hydrator code before it can hydrate
if (!this.hydrator) return;
// Make sure the island is mounted on the DOM before hydrating. It could be unmounted
// when the parent island hydrates and re-creates this island.
if (!this.isConnected) return;
// Wait for parent island to hydrate first so we hydrate top-down. The `ssr` attribute
// represents that it has not completed hydration yet.
const parentSsrIsland = this.parentElement?.closest('astro-island[ssr]');
if (parentSsrIsland) {
parentSsrIsland.addEventListener('astro:hydrate', this.hydrate, { once: true });
return;
}
const slotted = this.querySelectorAll('astro-slot');
const slots: Record<string, string> = {};
// Always check to see if there are templates.
// This happens if slots were passed but the client component did not render them.
const templates = this.querySelectorAll('template[data-astro-template]');
for (const template of templates) {
const closest = template.closest(this.tagName);
if (!closest?.isSameNode(this)) continue;
slots[template.getAttribute('data-astro-template') || 'default'] = template.innerHTML;
template.remove();
}
for (const slot of slotted) {
const closest = slot.closest(this.tagName);
if (!closest?.isSameNode(this)) continue;
slots[slot.getAttribute('name') || 'default'] = slot.innerHTML;
}
let props: Record<string, unknown>;
try {
props = this.hasAttribute('props')
? reviveObject(JSON.parse(this.getAttribute('props')!))
: {};
} catch (e) {
let componentName: string = this.getAttribute('component-url') || '<unknown>';
const componentExport = this.getAttribute('component-export');
if (componentExport) {
componentName += ` (export ${componentExport})`;
}
// eslint-disable-next-line no-console
console.error(
`[hydrate] Error parsing props for component ${componentName}`,
this.getAttribute('props'),
e
);
throw e;
}
let hydrationTimeStart;
const hydrator = this.hydrator(this);
if (process.env.NODE_ENV === 'development') hydrationTimeStart = performance.now();
await hydrator(this.Component, props, slots, {
client: this.getAttribute('client'),
});
if (process.env.NODE_ENV === 'development' && hydrationTimeStart)
this.setAttribute(
'client-render-time',
(performance.now() - hydrationTimeStart).toString()
);
this.removeAttribute('ssr');
this.dispatchEvent(new CustomEvent('astro:hydrate'));
};
attributeChangedCallback() {
this.hydrate();
}
unmount = () => {
// If element wasn't persisted, fire unmount event
if (!this.isConnected) this.dispatchEvent(new CustomEvent('astro:unmount'));
};
}
if (!customElements.get('astro-island')) {
customElements.define('astro-island', AstroIsland);
}
}