Move hydration directives to special attributes (#618)
* feat: ♻️ updating hydration to work with the directive syntax * test: ✅ Updating tests for the hydration directive syntax * refactor: Updating example projects for the hydration directive syntax * test: ✅ Found a test fixture still needing an update to the hydration directive syntax * style: Prettier strikes again! Reverting code formatting changes * refactor: ♻️ moving directive matching to a Set * refactor: Updating syntax to `client:load` * refactor: ♻️ Simplifying the `client:` directive match Per PR feedback from @matthewp * chore: errant console.warn() snuck into the last commit * feat: 🔊 Adding a super fancy build warning to update to the directive syntax * refactor: ♻️ Removing unnecessary checks when matching supported hydration directives `val` isn't being used for now, but leaving it in the attr destructuring as a reminder since it'll be needed for `client:media` * test: ✅ Including the original hydration syntax in a test to make sure it builds * style: 📝 Adding a comment to make it clear why the old hydration syntax is included in a the test markup * fix: 🐛 updating `head` logic to recognize hydration directive syntax * docs: Adding changeset * refactor: 🔥 Removing unnecessary `!hasComponents` check * docs: 📝 Adding more detail to the changset Co-authored-by: Tony Sullivan <tony.f.sullivan@gmail.com>
This commit is contained in:
parent
ea5afcd633
commit
0a7b6deaec
25 changed files with 117 additions and 59 deletions
15
.changeset/famous-years-bow.md
Normal file
15
.changeset/famous-years-bow.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
---
|
||||||
|
'astro': minor
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adds directive syntax for component hydration
|
||||||
|
|
||||||
|
This change updates the syntax for partial hydration from `<Button:load />` to `<Button client:load />`.
|
||||||
|
|
||||||
|
**Why?**
|
||||||
|
|
||||||
|
Partial hydration is about to get super powers! This clears the way for more dynamic partial hydration, i.e. `<MobileMenu client:media="(max-width: 40em)" />`.
|
||||||
|
|
||||||
|
**How to upgrade**
|
||||||
|
|
||||||
|
Just update `:load`, `:idle`, and `:visible` to match the `client:load` format, thats it! Don't worry, the original syntax is still supported but it's recommended to future-proof your project by updating to the newer syntax.
|
|
@ -54,7 +54,7 @@ Of course, sometimes client-side JavaScript is inevitable. Image carousels, shop
|
||||||
|
|
||||||
In other full-stack web frameworks this level of per-component optimization would be impossible without loading the entire page in JavaScript, delaying interactivity. In Astro, this kind of [partial hydration](https://addyosmani.com/blog/rehydration/) is built into the tool itself.
|
In other full-stack web frameworks this level of per-component optimization would be impossible without loading the entire page in JavaScript, delaying interactivity. In Astro, this kind of [partial hydration](https://addyosmani.com/blog/rehydration/) is built into the tool itself.
|
||||||
|
|
||||||
You can even [automatically defer components](https://codepen.io/jonneal/full/ZELvMvw) to only load once they become visible on the page with the `:visible` modifier.
|
You can even [automatically defer components](https://codepen.io/jonneal/full/ZELvMvw) to only load once they become visible on the page with the `client:visible` directive.
|
||||||
|
|
||||||
This new approach to web architecture is called [islands architecture](https://jasonformat.com/islands-architecture/). We didn't coin the term, but Astro may have perfected the technique. We are confident that an HTML-first, JavaScript-only-as-needed approach is the best solution for the majority of content-based websites.
|
This new approach to web architecture is called [islands architecture](https://jasonformat.com/islands-architecture/). We didn't coin the term, but Astro may have perfected the technique. We are confident that an HTML-first, JavaScript-only-as-needed approach is the best solution for the majority of content-based websites.
|
||||||
|
|
||||||
|
|
|
@ -212,7 +212,7 @@ const githubEditUrl = `https://github.com/USER/REPO/blob/main/${currentFile}`
|
||||||
<div />
|
<div />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<ThemeToggle:idle />
|
<ThemeToggle client:idle />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -232,7 +232,7 @@ const githubEditUrl = `https://github.com/USER/REPO/blob/main/${currentFile}`
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<aside class="sidebar" id="sidebar-content">
|
<aside class="sidebar" id="sidebar-content">
|
||||||
<DocSidebar:idle headers={headers} editHref={editHref} />
|
<DocSidebar client:idle headers={headers} editHref={editHref} />
|
||||||
</aside>
|
</aside>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -35,22 +35,22 @@ import SvelteCounter from '../components/SvelteCounter.svelte';
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
|
|
||||||
<react.Counter:visible>
|
<react.Counter client:visible>
|
||||||
<h1>Hello React!</h1>
|
<h1>Hello React!</h1>
|
||||||
<p>What's up?</p>
|
<p>What's up?</p>
|
||||||
</react.Counter:visible>
|
</react.Counter>
|
||||||
|
|
||||||
<PreactCounter:visible>
|
<PreactCounter client:visible>
|
||||||
<h1>Hello Preact!</h1>
|
<h1>Hello Preact!</h1>
|
||||||
</PreactCounter:visible>
|
</PreactCounter>
|
||||||
|
|
||||||
<VueCounter:visible>
|
<VueCounter client:visible>
|
||||||
<h1>Hello Vue!</h1>
|
<h1>Hello Vue!</h1>
|
||||||
</VueCounter:visible>
|
</VueCounter>
|
||||||
|
|
||||||
<SvelteCounter:visible>
|
<SvelteCounter client:visible>
|
||||||
<h1>Hello Svelte!</h1>
|
<h1>Hello Svelte!</h1>
|
||||||
</SvelteCounter:visible>
|
</SvelteCounter>
|
||||||
|
|
||||||
<A />
|
<A />
|
||||||
|
|
||||||
|
|
|
@ -33,9 +33,9 @@ import Counter from '../components/Counter.jsx'
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
<Counter:visible>
|
<Counter client:visible>
|
||||||
<h1>Hello Preact!</h1>
|
<h1>Hello Preact!</h1>
|
||||||
</Counter:visible>
|
</Counter>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -33,9 +33,9 @@ import Counter from '../components/Counter.jsx'
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
<Counter:visible>
|
<Counter client:visible>
|
||||||
<h1>Hello React!</h1>
|
<h1>Hello React!</h1>
|
||||||
</Counter:visible>
|
</Counter>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -33,9 +33,9 @@ import Counter from '../components/Counter.svelte'
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
<Counter:visible>
|
<Counter client:visible>
|
||||||
<h1>Hello Svelte!</h1>
|
<h1>Hello Svelte!</h1>
|
||||||
</Counter:visible>
|
</Counter>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -33,9 +33,9 @@ import Counter from '../components/Counter.vue'
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
<Counter:visible>
|
<Counter client:visible>
|
||||||
<h1>Hello Vue!</h1>
|
<h1>Hello Vue!</h1>
|
||||||
</Counter:visible>
|
</Counter>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -48,7 +48,7 @@ const description = 'Snowpack community news and companies that use Snowpack.';
|
||||||
working on!</div>
|
working on!</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
{news.reverse().map((item: any) => <Card:idle item={item} />)}
|
{news.reverse().map((item: any) => <Card client:idle item={item} />)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
|
|
|
@ -68,7 +68,7 @@ let description = 'Snowpack plugins allow for configuration-minimal tooling inte
|
||||||
|
|
||||||
<div style="margin-top:4rem;"></div>
|
<div style="margin-top:4rem;"></div>
|
||||||
|
|
||||||
<PluginSearchPage:load />
|
<PluginSearchPage client:load />
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
|
@ -32,10 +32,10 @@ const items = ['A', 'B', 'C'];
|
||||||
|
|
||||||
## Embed framework components
|
## Embed framework components
|
||||||
|
|
||||||
<ReactCounter:visible />
|
<ReactCounter client:visible />
|
||||||
<PreactCounter:visible />
|
<PreactCounter client:visible />
|
||||||
<VueCounter:visible />
|
<VueCounter client:visible />
|
||||||
<SvelteCounter:visible />
|
<SvelteCounter client:visible />
|
||||||
|
|
||||||
## Use Expressions
|
## Use Expressions
|
||||||
|
|
||||||
|
@ -43,11 +43,11 @@ const items = ['A', 'B', 'C'];
|
||||||
|
|
||||||
## Oh yeah...
|
## Oh yeah...
|
||||||
|
|
||||||
<ReactCounter:visible>
|
<ReactCounter client:visible>
|
||||||
🤯 It's also _recursive_!
|
🤯 It's also _recursive_!
|
||||||
|
|
||||||
### Markdown can be embedded in any child component
|
### Markdown can be embedded in any child component
|
||||||
</ReactCounter:visible>
|
</ReactCounter>
|
||||||
|
|
||||||
## Code
|
## Code
|
||||||
|
|
||||||
|
|
|
@ -37,10 +37,10 @@ import AdminsPreact from '../components/AdminsPreact.jsx';
|
||||||
<a href="https://github.com/nanostores/nanostores">nanostores</a></h1>
|
<a href="https://github.com/nanostores/nanostores">nanostores</a></h1>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<AdminsReact:load />
|
<AdminsReact client:load />
|
||||||
<AdminsSvelte:load />
|
<AdminsSvelte client:load />
|
||||||
<AdminsVue:load />
|
<AdminsVue client:load />
|
||||||
<AdminsPreact:load />
|
<AdminsPreact client:load />
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -118,9 +118,9 @@ TODO: Astro dynamic components guide
|
||||||
By default, Astro outputs zero client-side JS. If you'd like to include an interactive component in the client output, you may use any of the following techniques.
|
By default, Astro outputs zero client-side JS. If you'd like to include an interactive component in the client output, you may use any of the following techniques.
|
||||||
|
|
||||||
- `<MyComponent />` will render an HTML-only version of `MyComponent` (default)
|
- `<MyComponent />` will render an HTML-only version of `MyComponent` (default)
|
||||||
- `<MyComponent:load />` will render `MyComponent` on page load
|
- `<MyComponent client:load />` will render `MyComponent` on page load
|
||||||
- `<MyComponent:idle />` will use [requestIdleCallback()][mdn-ric] to render `MyComponent` as soon as main thread is free
|
- `<MyComponent client:idle />` will use [requestIdleCallback()][mdn-ric] to render `MyComponent` as soon as main thread is free
|
||||||
- `<MyComponent:visible />` will use an [IntersectionObserver][mdn-io] to render `MyComponent` when the element enters the viewport
|
- `<MyComponent client:visible />` will use an [IntersectionObserver][mdn-io] to render `MyComponent` when the element enters the viewport
|
||||||
|
|
||||||
### ⚛️ State Management
|
### ⚛️ State Management
|
||||||
|
|
||||||
|
|
|
@ -47,6 +47,23 @@ interface CodeGenOptions {
|
||||||
fileID: string;
|
fileID: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface HydrationAttributes {
|
||||||
|
method?: 'load' | 'idle' | 'visible';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Searches through attributes to extract hydration-rlated attributes */
|
||||||
|
function findHydrationAttributes(attrs: Record<string, string>): HydrationAttributes {
|
||||||
|
let method: HydrationAttributes['method'];
|
||||||
|
|
||||||
|
const hydrationDirectives = new Set(['client:load', 'client:idle', 'client:visible']);
|
||||||
|
|
||||||
|
for (const [key, val] of Object.entries(attrs)) {
|
||||||
|
if (hydrationDirectives.has(key)) method = key.slice(7) as HydrationAttributes['method'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { method };
|
||||||
|
}
|
||||||
|
|
||||||
/** Retrieve attributes from TemplateNode */
|
/** Retrieve attributes from TemplateNode */
|
||||||
async function getAttributes(attrs: Attribute[], state: CodegenState, compileOptions: CompileOptions): Promise<Record<string, string>> {
|
async function getAttributes(attrs: Attribute[], state: CodegenState, compileOptions: CompileOptions): Promise<Record<string, string>> {
|
||||||
let result: Record<string, string> = {};
|
let result: Record<string, string> = {};
|
||||||
|
@ -154,18 +171,32 @@ function getComponentUrl(astroConfig: AstroConfig, url: string, parentUrl: strin
|
||||||
interface GetComponentWrapperOptions {
|
interface GetComponentWrapperOptions {
|
||||||
filename: string;
|
filename: string;
|
||||||
astroConfig: AstroConfig;
|
astroConfig: AstroConfig;
|
||||||
|
compileOptions: CompileOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PlainExtensions = new Set(['.js', '.jsx', '.ts', '.tsx']);
|
const PlainExtensions = new Set(['.js', '.jsx', '.ts', '.tsx']);
|
||||||
/** Generate Astro-friendly component import */
|
/** Generate Astro-friendly component import */
|
||||||
function getComponentWrapper(_name: string, { url, importSpecifier }: ComponentInfo, opts: GetComponentWrapperOptions) {
|
function getComponentWrapper(_name: string, hydration: HydrationAttributes, { url, importSpecifier }: ComponentInfo, opts: GetComponentWrapperOptions) {
|
||||||
const { astroConfig, filename } = opts;
|
const { astroConfig, filename } = opts;
|
||||||
const [name, kind] = _name.split(':');
|
|
||||||
|
let name = _name;
|
||||||
|
let method = hydration.method;
|
||||||
|
|
||||||
|
/** Legacy support for original hydration syntax */
|
||||||
|
if (name.indexOf(':') > 0) {
|
||||||
|
const [legacyName, legacyMethod] = _name.split(':');
|
||||||
|
name = legacyName;
|
||||||
|
method = legacyMethod as HydrationAttributes['method'];
|
||||||
|
|
||||||
|
const { compileOptions, filename } = opts;
|
||||||
|
const shortname = path.posix.relative(compileOptions.astroConfig.projectRoot.pathname, filename);
|
||||||
|
warn(compileOptions.logging, shortname, yellow(`Deprecation warning: Partial hydration now uses a directive syntax. Please update to "<${name} client:${method} />"`));
|
||||||
|
}
|
||||||
|
|
||||||
// Special flow for custom elements
|
// Special flow for custom elements
|
||||||
if (isCustomElementTag(name)) {
|
if (isCustomElementTag(_name)) {
|
||||||
return {
|
return {
|
||||||
wrapper: `__astro_component(...__astro_element_registry.astroComponentArgs("${name}", ${JSON.stringify({ hydrate: kind, displayName: _name })}))`,
|
wrapper: `__astro_component(...__astro_element_registry.astroComponentArgs("${name}", ${JSON.stringify({ hydrate: method, displayName: _name })}))`,
|
||||||
wrapperImports: [
|
wrapperImports: [
|
||||||
`import {AstroElementRegistry} from 'astro/dist/internal/element-registry.js';`,
|
`import {AstroElementRegistry} from 'astro/dist/internal/element-registry.js';`,
|
||||||
`import {__astro_component} from 'astro/dist/internal/__astro_component.js';`,
|
`import {__astro_component} from 'astro/dist/internal/__astro_component.js';`,
|
||||||
|
@ -183,21 +214,21 @@ function getComponentWrapper(_name: string, { url, importSpecifier }: ComponentI
|
||||||
return { value: importSpecifier.imported.value };
|
return { value: importSpecifier.imported.value };
|
||||||
}
|
}
|
||||||
case 'ImportNamespaceSpecifier': {
|
case 'ImportNamespaceSpecifier': {
|
||||||
const [_, value] = name.split('.');
|
const [_, value] = _name.split('.');
|
||||||
return { value };
|
return { value };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const importInfo = kind
|
const importInfo = method
|
||||||
? {
|
? {
|
||||||
componentUrl: getComponentUrl(astroConfig, url, pathToFileURL(filename)),
|
componentUrl: getComponentUrl(astroConfig, url, pathToFileURL(filename)),
|
||||||
componentExport: getComponentExport(),
|
componentExport: getComponentExport()
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
wrapper: `__astro_component(${name}, ${JSON.stringify({ hydrate: kind, displayName: _name, ...importInfo })})`,
|
wrapper: `__astro_component(${name}, ${JSON.stringify({ hydrate: method, displayName: _name, ...importInfo })})`,
|
||||||
wrapperImports: [`import {__astro_component} from 'astro/dist/internal/__astro_component.js';`],
|
wrapperImports: [`import {__astro_component} from 'astro/dist/internal/__astro_component.js';`],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -633,6 +664,7 @@ async function compileHtml(enterNode: TemplateNode, state: CodegenState, compile
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const attributes = await getAttributes(node.attributes, state, compileOptions);
|
const attributes = await getAttributes(node.attributes, state, compileOptions);
|
||||||
|
const hydrationAttributes = findHydrationAttributes(attributes);
|
||||||
|
|
||||||
buffers.out += buffers.out === '' ? '' : ',';
|
buffers.out += buffers.out === '' ? '' : ',';
|
||||||
|
|
||||||
|
@ -671,7 +703,7 @@ async function compileHtml(enterNode: TemplateNode, state: CodegenState, compile
|
||||||
curr = 'markdown';
|
curr = 'markdown';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { wrapper, wrapperImports } = getComponentWrapper(name, componentInfo ?? ({} as any), { astroConfig, filename });
|
const { wrapper, wrapperImports } = getComponentWrapper(name, hydrationAttributes, componentInfo ?? ({} as any), { astroConfig, filename, compileOptions });
|
||||||
if (wrapperImports) {
|
if (wrapperImports) {
|
||||||
for (let wrapperImport of wrapperImports) {
|
for (let wrapperImport of wrapperImports) {
|
||||||
importStatements.add(wrapperImport);
|
importStatements.add(wrapperImport);
|
||||||
|
|
|
@ -21,8 +21,18 @@ export default function (opts: TransformOptions): Transformer {
|
||||||
},
|
},
|
||||||
InlineComponent: {
|
InlineComponent: {
|
||||||
enter(node) {
|
enter(node) {
|
||||||
|
if (hasComponents) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.attributes && node.attributes.some(({ name }: any) => name.startsWith('client:'))) {
|
||||||
|
hasComponents = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Check for legacy hydration */
|
||||||
const [_name, kind] = node.name.split(':');
|
const [_name, kind] = node.name.split(':');
|
||||||
if (kind && !hasComponents) {
|
if (kind) {
|
||||||
hasComponents = true;
|
hasComponents = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -7,6 +7,6 @@ import Tour from '../components/Tour.jsx';
|
||||||
<title>Stuff</title>
|
<title>Stuff</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<Tour:load />
|
<Tour client:load />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -5,8 +5,8 @@ import SvelteCounterRenamed from '../components/SvelteCounter.svelte';
|
||||||
<html>
|
<html>
|
||||||
<head><title>Dynamic pages</title></head>
|
<head><title>Dynamic pages</title></head>
|
||||||
<body>
|
<body>
|
||||||
<CounterRenamed:load />
|
<CounterRenamed client:load />
|
||||||
|
|
||||||
<SvelteCounterRenamed:load />
|
<SvelteCounterRenamed client:load />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -5,8 +5,9 @@ import SvelteCounter from '../components/SvelteCounter.svelte';
|
||||||
<html>
|
<html>
|
||||||
<head><title>Dynamic pages</title></head>
|
<head><title>Dynamic pages</title></head>
|
||||||
<body>
|
<body>
|
||||||
<Counter:load />
|
<Counter client:load />
|
||||||
|
|
||||||
|
<!-- Including the original hydration syntax to test backwards compatibility -->
|
||||||
<SvelteCounter:load />
|
<SvelteCounter:load />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -11,6 +11,6 @@ let title = 'My Page'
|
||||||
<body>
|
<body>
|
||||||
<h1>{title}</h1>
|
<h1>{title}</h1>
|
||||||
|
|
||||||
<Client:load />
|
<Client client:load />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -7,6 +7,6 @@ import Tour from '../components/Tour.jsx';
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div>Hello world</div>
|
<div>Hello world</div>
|
||||||
<Tour:load />
|
<Tour client:load />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -10,6 +10,6 @@ import Tour from '../components/Tour.jsx';
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div>Hello world</div>
|
<div>Hello world</div>
|
||||||
<Tour:load />
|
<Tour client:load />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -14,7 +14,7 @@ const description = 'This is a post about some stuff.';
|
||||||
## Interesting Topic
|
## Interesting Topic
|
||||||
|
|
||||||
<Hello name={`world`} />
|
<Hello name={`world`} />
|
||||||
<Counter:load />
|
<Counter client:load />
|
||||||
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</Markdown>
|
</Markdown>
|
||||||
|
|
|
@ -10,6 +10,6 @@ import '../components/my-element.js';
|
||||||
<body>
|
<body>
|
||||||
<h1>{title}</h1>
|
<h1>{title}</h1>
|
||||||
|
|
||||||
<my-element:load></my-element:load>
|
<my-element client:load></my-element>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -7,4 +7,4 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div>Something here</div>
|
<div>Something here</div>
|
||||||
<Something:idle />
|
<Something client:idle />
|
|
@ -10,5 +10,5 @@ import Child from '../components/Child.astro';
|
||||||
</style>
|
</style>
|
||||||
<h1>Title of this Blog</h1>
|
<h1>Title of this Blog</h1>
|
||||||
|
|
||||||
<Something:load />
|
<Something client:load />
|
||||||
<Child />
|
<Child />
|
Loading…
Reference in a new issue