native-sfc
v0.0.3
Published
Load a single HTML file as a Web Component.
Readme
Native-SFC
Load a single HTML file as a Web Component.
<!-- index.html -->
<my-counter></my-counter>
<script type="module">
import { loadComponent } from "https://esm.sh/native-sfc";
loadComponent("my-counter", "./my-counter.html");
</script>Use export default to declare the component class.
<!-- my-counter.html -->
<button @click="setCount(count() + 1)">Count is: {{ count() }}</button>
<script type="module">
import { signal } from "https://esm.sh/native-sfc";
export default class MyCounter extends HTMLElement {
setup() {
const [count, setCount] = signal(0);
return { count, setCount };
}
connectedCallback() {/* TODO */}
}
// OR
import { defineComponent } from "https://esm.sh/native-sfc";
export default defineComponent(({ onConnected }) => {
const [count, setCount] = signal(0);
onConnected(() => {/* TODO */});
return { count, setCount };
});
</script>How it works
The component loader fetches everything (HTML, JS, CSS) as text, then processes them to create a Web Component.
- HTML Parsing: The HTML file is parsed and processed
- Style Handling: All
<style>and<link rel="stylesheet">are collected and converted toadoptedStyleSheetsof the component's shadow root (avoids FOUC) - Script Handling: ESM modules (
<script type="module">) are evaluated and their exports are merged. Thedefaultexport, if it's a class extendingHTMLElement, becomes the base class of the component - Global Styles: Styles with
globalattribute are moved to the outer document instead of the shadow root - URL Rewriting: All relative URLs in
srcandhrefattributes are rewritten to absolute URLs based on the component file location
Component API
loadComponent(name: string, url: string)
Load a component from a HTML file and register it as a custom element.
name: The custom element name (e.g.,"my-component")url: The URL to the component HTML file (relative to the importer)
defineComponent(setup: ({ onConnected?, onDisconnected? }) => void)
A helper function for dual-mode component definition:
- When used inside a
loadComponent-imported module, it defines a web component class with lifecycle callbacks - When used in normal document context, it runs the setup function with
documentas root - The setup function receives
{ onConnected, onDisconnected }callbacks - Return an object from setup to expose reactive state to the template
Signals API
signal<T>(initialValue: T): [() => T, (v: T) => void]
Creates a reactive signal with a getter and setter.
- Returns a tuple:
[getter, setter] getter(): Returns the current valuesetter(value): Updates the signal value and triggers reactivity
computed<T>(fn: () => T): () => T
Creates a computed value that automatically tracks dependencies.
fn: Function that computes and returns the value- Returns a getter function that returns the computed result
- Automatically updates when dependencies change
effect(fn: VoidFunction): VoidFunction
Creates a reactive effect that runs whenever its dependencies change.
fn: Function to execute- Returns a cleanup function to stop the effect
- Useful for side effects and subscriptions
effectScope(fn: VoidFunction): VoidFunction
Creates an effect scope to batch multiple effects together.
fn: Function containing effect definitions- Returns a cleanup function to stop all effects in the scope
- Useful for organizing related effects
HTML Template API
.property
Binds a DOM property to a reactive expression.
<input .value="someSignal()" />:attribute
Binds a DOM attribute to a reactive expression.
<img :src="imageUrl()" />@event
Binds a DOM event to a reactive expression.
<button @click="handleClick()" />{{ expression }}
Embeds a reactive expression inside text content.
<p>Total: {{ total() }}</p>#if="condition"
Conditionally renders an element based on a reactive expression.
<div #if="isVisible()">This content is visible only if isVisible() is true.</div>#for="arrayExpression"
Renders a list of elements based on a reactive array expression.
<li #for="items().map(item => ({ item }))">{{ item.name }}</li>Limitations
- Dynamic imports with relative paths are NOT supported.
- Inside the ESM modules, the
fromsyntax in import statements are rewritten to absolute URL, since all modules are actually loaded as blob URLs. - Since all styles are moved to
adoptedStyleSheetsin component's shadow root, we CANNOT use@importrules in styles. - Relative URLs in CSS (e.g.,
background-image: url(./bg.png)) are resolved relative to the main document (the page URL), not the component file URL. - Components loaded with the same name and URL are cached and reused.
- Only
script[src]'s src'link[rel="stylesheet"]'s href will be rewritten. Warn thata[href]/img[src]/etc with relative URLs in the HTML body will NOT be rewritten.
Next Steps
- Implement component debugger
- Implement component bundler in both runtime and server-side
- Consider to support rewriting relative URLs in CSS
