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:
parent
8e6eb624ae
commit
b0de82b1e9
1 changed files with 165 additions and 161 deletions
|
@ -44,170 +44,174 @@ declare const Astro: {
|
||||||
return Object.fromEntries(Object.entries(raw).map(([key, value]) => [key, reviveTuple(value)]));
|
return Object.fromEntries(Object.entries(raw).map(([key, value]) => [key, reviveTuple(value)]));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!customElements.get('astro-island')) {
|
// 🌊🏝️🌴
|
||||||
customElements.define(
|
class AstroIsland extends HTMLElement {
|
||||||
'astro-island',
|
public Component: any;
|
||||||
class extends HTMLElement {
|
public hydrator: any;
|
||||||
public Component: any;
|
static observedAttributes = ['props'];
|
||||||
public hydrator: any;
|
|
||||||
static observedAttributes = ['props'];
|
disconnectedCallback() {
|
||||||
disconnectedCallback() {
|
document.removeEventListener('astro:after-swap', this.unmount);
|
||||||
document.removeEventListener('astro:after-swap', this.unmount);
|
document.addEventListener('astro:after-swap', this.unmount, { once: true });
|
||||||
document.addEventListener('astro:after-swap', this.unmount, { once: true });
|
}
|
||||||
}
|
|
||||||
connectedCallback() {
|
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 (
|
if (
|
||||||
!this.hasAttribute('await-children') ||
|
this.lastChild?.nodeType === Node.COMMENT_NODE &&
|
||||||
document.readyState === 'interactive' ||
|
this.lastChild.nodeValue === 'astro:end'
|
||||||
document.readyState === 'complete'
|
|
||||||
) {
|
) {
|
||||||
this.childrenConnectedCallback();
|
this.lastChild.remove();
|
||||||
} else {
|
onConnected();
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
async childrenConnectedCallback() {
|
mo.observe(this, { childList: true });
|
||||||
let beforeHydrationUrl = this.getAttribute('before-hydration-url');
|
// in case the marker comment got stripped and the mutation observer waited indefinitely,
|
||||||
if (beforeHydrationUrl) {
|
// also wait for DOMContentLoaded as a last resort
|
||||||
await import(beforeHydrationUrl);
|
document.addEventListener('DOMContentLoaded', onConnected);
|
||||||
}
|
|
||||||
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'));
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue