@zeix/ui-element
Version:
UIElement - minimal reactive framework based on Web Components
88 lines (79 loc) • 1.76 kB
text/typescript
import {
type AttributeParser,
type Component,
type SignalProducer,
setProperty,
setText,
dangerouslySetInnerHTML,
component,
first,
} from "../../../";
export type LazyLoadProps = {
error: string;
src: string;
content: string;
};
/* === Attribute Parser === */
const asURL: AttributeParser<HTMLElement & { error: string }, string> = (
el,
v,
) => {
let value = "";
let error = "";
if (!v) {
error = "No URL provided in src attribute";
} else if (
(el.parentElement || (el.getRootNode() as ShadowRoot).host)?.closest(
`${el.localName}[src="${v}"]`,
)
) {
error = "Recursive loading detected";
} else {
const url = new URL(v, location.href); // Ensure 'src' attribute is a valid URL
if (url.origin === location.origin)
value = String(url); // Sanity check for cross-origin URLs
else error = "Invalid URL origin";
}
el.error = error;
return value;
};
/* === Signal Producer === */
const fetchText: SignalProducer<
HTMLElement & { error: string; src: string },
string
> = (el) => async (abort) => {
// Async Computed callback
const url = el.src;
if (!url) return "";
try {
const response = await fetch(url, { signal: abort });
el.querySelector(".loading")?.remove();
if (response.ok) return response.text();
else el.error = response.statusText;
} catch (error) {
el.error = error.message;
}
return "";
};
/* === Component === */
export default component(
"lazy-load",
{
error: "",
src: asURL,
content: fetchText,
},
(el) => [
dangerouslySetInnerHTML("content"),
first<LazyLoadProps, HTMLElement>(
".error",
setText("error"),
setProperty("hidden", () => !el.error),
),
],
);
declare global {
interface HTMLElementTagNameMap {
"lazy-load": Component<LazyLoadProps>;
}
}