Skip to content

Commit dd13f51

Browse files
committed
feature parity again
1 parent cc0dbbe commit dd13f51

4 files changed

Lines changed: 156 additions & 130 deletions

File tree

apps/fixtures/image/src/routes/index.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,20 @@
1-
import { Image } from "@solidjs/image";
1+
import { StartImage as Image } from "@solidjs/image";
22
import { Title } from "@solidjs/meta";
3+
import { type JSX, onMount, Show } from "solid-js";
34
import imageData from "../images/example.jpg?image";
45

6+
interface PlaceholderProps {
7+
show: () => void;
8+
}
9+
10+
function Placeholder(props: PlaceholderProps): JSX.Element {
11+
onMount(() => {
12+
props.show();
13+
});
14+
15+
return <div>Loading...</div>;
16+
}
17+
518
export default function Home() {
619
return (
720
<main>
@@ -12,8 +25,13 @@ export default function Home() {
1225
</p>
1326
<div style={{ width: "60vw", "margin-left": "auto", "margin-right": "auto", background: "white", padding: "1rem" }}>
1427
<Image
15-
src={imageData}
28+
{...imageData}
1629
alt="Example"
30+
fallback={(visible, show) => (
31+
<Show when={visible()}>
32+
<Placeholder show={show} />
33+
</Show>
34+
)}
1735
/>
1836
</div>
1937
</main>

packages/image/src/Image.tsx

Lines changed: 100 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,118 @@
11
import type { JSX } from "solid-js";
2-
import { ssr as ssrHtml, isServer } from "solid-js/web";
3-
import type { StartImageSource, StartImageTransformer, StartImageVariant } from "./types";
2+
import { createMemo, createSignal, For, Show } from "solid-js";
3+
import { ClientOnly } from "./client-only.tsx";
4+
import { createLazyRender } from "./create-lazy-render.ts";
5+
import {
6+
createImageVariants,
7+
mergeImageVariantsByType,
8+
mergeImageVariantsToSrcSet,
9+
} from "./transformer.ts";
10+
import type { StartImageSource, StartImageTransformer, StartImageVariant } from "./types.ts";
11+
import { getAspectRatioBoxStyle } from "./utils.ts";
412

5-
export interface ImageProps<T> {
6-
src: {
7-
src: StartImageSource<T>
8-
transformer?: StartImageTransformer<T>
9-
}
13+
import "./styles.css";
14+
15+
export interface StartImageProps<T> {
16+
src: StartImageSource<T>;
1017
alt: string;
11-
fallback?: (visible: () => boolean, onLoad: () => void) => JSX.Element;
12-
}
18+
transformer?: StartImageTransformer<T>;
1319

14-
function mergeImageVariantsByType(variants: StartImageVariant[]) {
15-
const map = new Map<string, StartImageVariant[]>();
16-
for (const variant of variants) {
17-
const arr = map.get(variant.type) || [];
18-
arr.push(variant);
19-
map.set(variant.type, arr);
20-
}
21-
return map;
20+
onLoad?: () => void;
21+
fallback: (visible: () => boolean, onLoad: () => void) => JSX.Element;
22+
23+
crossOrigin?: JSX.HTMLCrossorigin | undefined;
24+
fetchPriority?: "high" | "low" | "auto" | undefined;
25+
decoding?: "sync" | "async" | "auto" | undefined;
2226
}
2327

24-
function mergeImageVariantsToSrcSet(variants: StartImageVariant[]) {
25-
return variants.map(v => `${v.path} ${v.width}w`).join(",");
28+
interface StartImageSourcesProps<T> extends StartImageProps<T> {
29+
variants: StartImageVariant[];
2630
}
2731

28-
export function Image<T>(props: ImageProps<T>): JSX.Element {
29-
if (isServer) {
30-
const variants = props.src.transformer
31-
? props.src.transformer.transform(props.src.src)
32-
: [];
33-
const variantArray = Array.isArray(variants) ? variants : [variants];
34-
35-
let html = `<div data-start-image="container">`;
36-
html += `<div data-start-image="aspect-ratio" style="position:relative;padding-top:${(props.src.src.height * 100) / props.src.src.width}%;width:100%;height:0;overflow:hidden;">`;
37-
html += `<picture data-start-picture="picture">`;
38-
39-
if (variantArray.length > 0) {
40-
const merged = mergeImageVariantsByType(variantArray);
41-
for (const [type, vars] of merged) {
42-
const srcset = mergeImageVariantsToSrcSet(vars);
43-
html += `<source type="${type}" srcset="${srcset}"/>`;
44-
}
32+
function StartImageSources<T>(props: StartImageSourcesProps<T>): JSX.Element {
33+
const mergedVariants = createMemo(() => {
34+
const types = mergeImageVariantsByType(props.variants);
35+
36+
const values: [type: string, srcset: string][] = [];
37+
38+
for (const [key, variants] of types) {
39+
values.push([key, mergeImageVariantsToSrcSet(variants)]);
4540
}
46-
47-
html += `<img data-start-image="image" src="${props.src.src.source}" alt="${props.alt}" style="position:absolute;top:0;left:0;width:100%;height:100%;object-fit:contain;"/>`;
48-
html += `</picture></div></div>`;
49-
50-
return ssrHtml(html) as unknown as JSX.Element;
41+
42+
return values;
43+
});
44+
45+
return (
46+
<For each={mergedVariants()}>{([type, srcset]) => <source type={type} srcset={srcset} />}</For>
47+
);
48+
}
49+
50+
export function StartImage<T>(props: StartImageProps<T>): JSX.Element {
51+
const [showPlaceholder, setShowPlaceholder] = createSignal(true);
52+
const laze = createLazyRender<HTMLDivElement>();
53+
const [defer, setDefer] = createSignal(true);
54+
55+
function onPlaceholderLoad() {
56+
setDefer(false);
5157
}
52-
58+
59+
const width = createMemo(() => props.src.width);
60+
const height = createMemo(() => props.src.height);
61+
5362
return (
54-
<div data-start-image="container">
55-
<div
56-
data-start-image="aspect-ratio"
57-
style={{
58-
position: "relative",
59-
"padding-top": `${(props.src.src.height * 100) / props.src.src.width}%`,
60-
width: "100%",
61-
height: "0",
62-
overflow: "hidden",
63-
}}
63+
<div ref={laze.ref} data-start-image="container">
64+
<div
65+
data-start-image="aspect-ratio"
66+
style={getAspectRatioBoxStyle({
67+
width: width(),
68+
height: height(),
69+
})}
6470
>
65-
<picture data-start-picture="picture">
66-
{props.src.transformer && (() => {
67-
const variants = props.src.transformer!.transform(props.src.src);
68-
const variantArray = Array.isArray(variants) ? variants : [variants];
69-
const merged = mergeImageVariantsByType(variantArray);
70-
return Array.from(merged).map(([type, vars]) => (
71-
<source
72-
type={type}
73-
srcset={mergeImageVariantsToSrcSet(vars)}
71+
<picture data-start-image="picture">
72+
<Show when={props.transformer} fallback={<source src={props.src.source} />}>
73+
{cb => <StartImageSources variants={createImageVariants(props.src, cb())} {...props} />}
74+
</Show>
75+
<ClientOnly
76+
fallback={
77+
<img
78+
data-start-image="image"
79+
alt={props.alt}
80+
crossOrigin={props.crossOrigin}
81+
fetchpriority={props.fetchPriority}
82+
decoding={props.decoding}
7483
/>
75-
));
76-
})()}
77-
<img
78-
data-start-image="image"
79-
src={props.src.src.source}
80-
alt={props.alt}
81-
style={{
82-
position: "absolute",
83-
top: "0",
84-
left: "0",
85-
width: "100%",
86-
height: "100%",
87-
"object-fit": "contain",
88-
}}
89-
/>
84+
}
85+
>
86+
<Show when={laze.visible}>
87+
<img
88+
data-start-image="image"
89+
// src={getEmptyImageURL({
90+
// width: width(),
91+
// height: height(),
92+
// })}
93+
alt={props.alt}
94+
onLoad={() => {
95+
if (!defer()) {
96+
setShowPlaceholder(false);
97+
props.onLoad?.();
98+
}
99+
}}
100+
style={{
101+
opacity: showPlaceholder() ? 0 : 1,
102+
}}
103+
crossOrigin={props.crossOrigin}
104+
fetchpriority={props.fetchPriority}
105+
decoding={props.decoding}
106+
/>
107+
</Show>
108+
</ClientOnly>
90109
</picture>
91110
</div>
111+
<div data-start-image="blocker">
112+
<ClientOnly>
113+
<Show when={laze.visible}>{props.fallback(showPlaceholder, onPlaceholderLoad)}</Show>
114+
</ClientOnly>
115+
</div>
92116
</div>
93117
);
94118
}
Lines changed: 33 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { createSignal } from "solid-js";
2-
import { isServer } from "solid-js/web";
1+
import { createEffect, createSignal, onCleanup } from "solid-js";
32

43
export interface LazyRender<T extends HTMLElement> {
54
ref: (value: T) => void;
@@ -14,66 +13,51 @@ export function createLazyRender<T extends HTMLElement>(
1413
options?: LazyRenderOptions,
1514
): LazyRender<T> {
1615
const [visible, setVisible] = createSignal(false);
17-
let element: T | null = null;
18-
let observer: IntersectionObserver | null = null;
1916

20-
function setupObserver(el: T) {
21-
if (isServer) return;
22-
23-
if (observer) {
24-
observer.disconnect();
17+
// We use a reactive ref here so that the component
18+
// re-renders if the host element changes, therefore
19+
// re-evaluating our intersection logic
20+
const [ref, setRef] = createSignal<T | null>(null);
21+
22+
createEffect(() => {
23+
// If the host changed, make sure that
24+
// visibility is set to false
25+
setVisible(false);
26+
const shouldRefresh = options?.refresh;
27+
28+
const current = ref();
29+
if (!current) {
30+
return;
2531
}
26-
27-
observer = new IntersectionObserver(entries => {
28-
console.log("[lazy-render] intersection:", entries);
32+
33+
const observer = new IntersectionObserver(entries => {
2934
for (const entry of entries) {
30-
if (entry.isIntersecting) {
31-
console.log("[lazy-render] element is intersecting, setting visible to true");
35+
if (shouldRefresh) {
36+
setVisible(entry.isIntersecting);
37+
} else if (entry.isIntersecting) {
38+
// Host intersected, set visibility to true
3239
setVisible(true);
33-
observer?.disconnect();
40+
41+
// Stop observing
42+
observer.disconnect();
3443
}
3544
}
3645
});
3746

38-
observer.observe(el);
39-
40-
// Check immediately in case element is already visible
41-
requestAnimationFrame(() => {
42-
if (!el || !observer) return;
43-
const rect = el.getBoundingClientRect();
44-
const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
45-
console.log("[lazy-render] rect:", rect, "isVisible:", isVisible);
46-
if (isVisible) {
47-
setVisible(true);
48-
observer.disconnect();
49-
}
50-
});
47+
observer.observe(current);
5148

52-
// Also check after a short delay
53-
setTimeout(() => {
54-
if (!el || !observer) return;
55-
const rect = el.getBoundingClientRect();
56-
const isVisible = rect.top < window.innerHeight && rect.bottom > 0;
57-
console.log("[lazy-render] delayed check - isVisible:", isVisible);
58-
if (isVisible) {
59-
setVisible(true);
60-
observer.disconnect();
61-
}
62-
}, 100);
63-
}
49+
onCleanup(() => {
50+
observer.unobserve(current);
51+
observer.disconnect();
52+
});
53+
});
6454

6555
return {
66-
ref(value: T) {
67-
console.log("[lazy-render] ref called with:", value);
68-
element = value;
69-
if (element && !isServer) {
70-
setupObserver(element);
71-
}
56+
ref(value) {
57+
return setRef(() => value);
7258
},
7359
get visible() {
74-
const v = visible();
75-
console.log("[lazy-render] visible getter:", v);
76-
return v;
60+
return visible();
7761
},
7862
};
7963
}

packages/image/src/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export { Image } from "./Image.tsx";
2-
export { ClientOnly } from "./client-only";
3-
export type { ImageProps } from "./Image.tsx";
1+
export { StartImage } from "./Image.tsx";
2+
export { ClientOnly } from "./client-only.tsx";
3+
export type { StartImageProps } from "./Image.tsx";

0 commit comments

Comments
 (0)