autosuspense 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -2
- package/package.json +1 -1
- package/src/components/index.ts +2 -2
- package/src/components/prefabs/defaultPrefab.tsx +10 -0
- package/src/components/prefabs/prebuilt/Block.css +42 -0
- package/src/components/prefabs/prebuilt/Block.tsx +16 -0
- package/src/components/prefabs/prebuilt/Card.css +45 -0
- package/src/components/prefabs/prebuilt/Card.tsx +16 -0
- package/src/components/prefabs/prebuilt/List.css +48 -0
- package/src/components/prefabs/prebuilt/List.tsx +16 -0
- package/src/components/{AutoSuspense.tsx → suspense/AutoSuspense.tsx} +14 -4
- package/src/components/{GeneratedFallback.tsx → suspense/GeneratedFallback.tsx} +4 -4
- package/src/hooks/useSuspenseProvider.ts +0 -0
- package/src/types/FallbackRegistry.ts +11 -3
- package/src/utils/renderNode.ts +10 -5
- package/src/utils/resolveElement.ts +32 -0
- package/tsconfig.json +5 -5
- package/autosuspense-lib-1.0.0.tgz +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
# AutoSupense
|
|
2
|
-
AutoSuspense is a
|
|
2
|
+
AutoSuspense is a small React utility that automatically builds and composes
|
|
3
|
+
Suspense fallback UI based on your component tree without manually wiring
|
|
4
|
+
nested fallback components.
|
|
3
5
|
|
|
4
6
|
## How to use?:
|
|
5
7
|
1. Install this library
|
|
6
|
-
```npm i autosuspense```
|
|
8
|
+
```npm i autosuspense```
|
|
9
|
+
|
|
10
|
+
2. Use it by calling and passing your components normally like ```<Supense>```:
|
|
11
|
+
```
|
|
12
|
+
import { AutoSuspense } from "autosuspense";
|
|
13
|
+
|
|
14
|
+
function App() {
|
|
15
|
+
return (
|
|
16
|
+
<AutoSuspense>
|
|
17
|
+
<Page />
|
|
18
|
+
</AutoSuspense>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
3. Inside each AutoSuspense component provide a fallback componet for that particular component via ```useSuspenseFallback``` hook.
|
|
24
|
+
```
|
|
25
|
+
import { useSuspenseFallback } from "autosuspense";
|
|
26
|
+
|
|
27
|
+
function UserCard() {
|
|
28
|
+
useSuspenseFallback(<div>Loading user…</div>);
|
|
29
|
+
|
|
30
|
+
// Component may suspend somewhere below
|
|
31
|
+
return <Profile />;
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Important Caveats:
|
|
36
|
+
1. This library works on top of existing Suspense blocks and prebuilds your Fallback UI via render tree traversal.
|
|
37
|
+
2. This means that it uses Depth First searching to prebuild so react is only able to show prebuilt Ui when it first encounters a place to suspend. This could mean that rest of your fallback UI in next adjacent components never render.
|
|
38
|
+
|
|
39
|
+
### TBD:
|
|
40
|
+
1. Adding Default Skeletons.
|
|
41
|
+
2. Supporting existing Library skeleton implementations.
|
|
42
|
+
3. Adding a central config & template extension components & file to easily extend and manage your Fallback UI.
|
package/package.json
CHANGED
package/src/components/index.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { AutoSuspense } from "./AutoSuspense";
|
|
2
|
-
import { GeneratedFallback } from "./GeneratedFallback";
|
|
1
|
+
import { AutoSuspense } from "./suspense/AutoSuspense";
|
|
2
|
+
import { GeneratedFallback } from "./suspense/GeneratedFallback";
|
|
3
3
|
export { AutoSuspense, GeneratedFallback };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Block } from "./prebuilt/Block";
|
|
3
|
+
import { Card } from "./prebuilt/Card";
|
|
4
|
+
import { List } from "./prebuilt/List";
|
|
5
|
+
|
|
6
|
+
export const defaultPrefab: Record<string, React.ReactElement> = {
|
|
7
|
+
block: <Block />,
|
|
8
|
+
card: <Card />,
|
|
9
|
+
list: <List />,
|
|
10
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
.as-block {
|
|
2
|
+
width: 100%;
|
|
3
|
+
min-height: var(--as-block-min-height, 96px);
|
|
4
|
+
border-radius: var(--as-block-radius, 12px);
|
|
5
|
+
box-sizing: border-box;
|
|
6
|
+
|
|
7
|
+
background: var(
|
|
8
|
+
--as-block-bg,
|
|
9
|
+
linear-gradient(
|
|
10
|
+
90deg,
|
|
11
|
+
var(--as-skeleton-base, #ececec) 25%,
|
|
12
|
+
var(--as-skeleton-highlight, #f5f5f5) 37%,
|
|
13
|
+
var(--as-skeleton-base, #ececec) 63%
|
|
14
|
+
)
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
background-size: 400% 100%;
|
|
18
|
+
animation:
|
|
19
|
+
as-shimmer 1.4s ease infinite,
|
|
20
|
+
as-pulse 2s ease-in-out infinite;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@keyframes as-shimmer {
|
|
24
|
+
0% {
|
|
25
|
+
background-position: 100% 0;
|
|
26
|
+
}
|
|
27
|
+
100% {
|
|
28
|
+
background-position: 0 0;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@keyframes as-pulse {
|
|
33
|
+
0% {
|
|
34
|
+
opacity: 1;
|
|
35
|
+
}
|
|
36
|
+
50% {
|
|
37
|
+
opacity: 0.85;
|
|
38
|
+
}
|
|
39
|
+
100% {
|
|
40
|
+
opacity: 1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import "./Block.css";
|
|
3
|
+
|
|
4
|
+
type BlockProps = {
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
style?: React.CSSProperties;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const Block: React.FC<BlockProps> = ({ children, className, style }) => {
|
|
11
|
+
return (
|
|
12
|
+
<div className={`as-block ${className ?? ""}`} style={style} data-as-block>
|
|
13
|
+
{children}
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
.as-card {
|
|
2
|
+
width: 100%;
|
|
3
|
+
min-height: var(--as-card-min-height, 120px);
|
|
4
|
+
padding: var(--as-card-padding, 16px);
|
|
5
|
+
box-sizing: border-box;
|
|
6
|
+
|
|
7
|
+
border-radius: var(--as-card-radius, 12px);
|
|
8
|
+
|
|
9
|
+
background: var(
|
|
10
|
+
--as-card-bg,
|
|
11
|
+
linear-gradient(
|
|
12
|
+
90deg,
|
|
13
|
+
var(--as-skeleton-base, #ececec) 25%,
|
|
14
|
+
var(--as-skeleton-highlight, #f5f5f5) 37%,
|
|
15
|
+
var(--as-skeleton-base, #ececec) 63%
|
|
16
|
+
)
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
background-size: 400% 100%;
|
|
20
|
+
animation:
|
|
21
|
+
as-shimmer 1.4s ease infinite,
|
|
22
|
+
as-pulse 2s ease-in-out infinite;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/* reuse same animations */
|
|
26
|
+
@keyframes as-shimmer {
|
|
27
|
+
0% {
|
|
28
|
+
background-position: 100% 0;
|
|
29
|
+
}
|
|
30
|
+
100% {
|
|
31
|
+
background-position: 0 0;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@keyframes as-pulse {
|
|
36
|
+
0% {
|
|
37
|
+
opacity: 1;
|
|
38
|
+
}
|
|
39
|
+
50% {
|
|
40
|
+
opacity: 0.85;
|
|
41
|
+
}
|
|
42
|
+
100% {
|
|
43
|
+
opacity: 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import "./Card.css";
|
|
3
|
+
|
|
4
|
+
type CardProps = {
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
style?: React.CSSProperties;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const Card: React.FC<CardProps> = ({ children, className, style }) => {
|
|
11
|
+
return (
|
|
12
|
+
<div className={`as-card ${className ?? ""}`} style={style} data-as-card>
|
|
13
|
+
{children}
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
.as-list {
|
|
2
|
+
width: 100%;
|
|
3
|
+
min-height: var(--as-list-min-height, 160px);
|
|
4
|
+
padding: var(--as-list-padding, 12px);
|
|
5
|
+
box-sizing: border-box;
|
|
6
|
+
|
|
7
|
+
border-radius: var(--as-list-radius, 8px);
|
|
8
|
+
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
gap: var(--as-list-gap, 12px);
|
|
12
|
+
|
|
13
|
+
background: var(
|
|
14
|
+
--as-list-bg,
|
|
15
|
+
linear-gradient(
|
|
16
|
+
90deg,
|
|
17
|
+
var(--as-skeleton-base, #ececec) 25%,
|
|
18
|
+
var(--as-skeleton-highlight, #f5f5f5) 37%,
|
|
19
|
+
var(--as-skeleton-base, #ececec) 63%
|
|
20
|
+
)
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
background-size: 400% 100%;
|
|
24
|
+
animation:
|
|
25
|
+
as-shimmer 1.4s ease infinite,
|
|
26
|
+
as-pulse 2s ease-in-out infinite;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@keyframes as-shimmer {
|
|
30
|
+
0% {
|
|
31
|
+
background-position: 100% 0;
|
|
32
|
+
}
|
|
33
|
+
100% {
|
|
34
|
+
background-position: 0 0;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@keyframes as-pulse {
|
|
39
|
+
0% {
|
|
40
|
+
opacity: 1;
|
|
41
|
+
}
|
|
42
|
+
50% {
|
|
43
|
+
opacity: 0.85;
|
|
44
|
+
}
|
|
45
|
+
100% {
|
|
46
|
+
opacity: 1;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import "./List.css";
|
|
3
|
+
|
|
4
|
+
type ListProps = {
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
style?: React.CSSProperties;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const List: React.FC<ListProps> = ({ children, className, style }) => {
|
|
11
|
+
return (
|
|
12
|
+
<div className={`as-list ${className ?? ""}`} style={style} data-as-list>
|
|
13
|
+
{children}
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { GeneratedFallback } from "./GeneratedFallback";
|
|
2
4
|
import {
|
|
3
5
|
FallbackContext,
|
|
4
|
-
|
|
5
|
-
} from "
|
|
6
|
-
import {
|
|
6
|
+
FallbackRegistry,
|
|
7
|
+
} from "../../types/FallbackRegistry";
|
|
8
|
+
import { defaultPrefab } from "../prefabs/defaultPrefab";
|
|
7
9
|
|
|
8
|
-
export const AutoSuspense = ({
|
|
10
|
+
export const AutoSuspense = ({
|
|
11
|
+
children,
|
|
12
|
+
prefab = defaultPrefab,
|
|
13
|
+
}: {
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
prefab?: Record<string, React.ReactElement>;
|
|
16
|
+
}) => {
|
|
9
17
|
const registryRef = React.useRef<FallbackRegistry>({
|
|
10
18
|
nodes: new Map(),
|
|
11
19
|
currentParent: null,
|
|
20
|
+
prebuild: new Map(Object.entries(prefab)),
|
|
12
21
|
});
|
|
13
22
|
|
|
14
23
|
if (registryRef.current === null) {
|
|
15
24
|
registryRef.current = {
|
|
16
25
|
nodes: new Map(),
|
|
17
26
|
currentParent: null,
|
|
27
|
+
prebuild: new Map(Object.entries(prefab)),
|
|
18
28
|
};
|
|
19
29
|
}
|
|
20
30
|
const fallback = <GeneratedFallback registry={registryRef.current} />;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import
|
|
3
|
-
import { buildTree } from "
|
|
4
|
-
import { renderNode } from "
|
|
2
|
+
import { FallbackRegistry } from "../../types/FallbackRegistry";
|
|
3
|
+
import { buildTree } from "../../utils/buildTree";
|
|
4
|
+
import { renderNode } from "../../utils/renderNode";
|
|
5
5
|
|
|
6
6
|
export function GeneratedFallback({
|
|
7
7
|
registry,
|
|
@@ -13,7 +13,7 @@ export function GeneratedFallback({
|
|
|
13
13
|
|
|
14
14
|
return (
|
|
15
15
|
<React.Fragment>
|
|
16
|
-
{roots.map((node) => renderNode(node, tree))}
|
|
16
|
+
{roots.map((node) => renderNode(node, tree, registry))}
|
|
17
17
|
</React.Fragment>
|
|
18
18
|
);
|
|
19
19
|
}
|
|
File without changes
|
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as React from "react";
|
|
2
2
|
|
|
3
3
|
export type Node = {
|
|
4
4
|
id: string;
|
|
5
|
-
element:
|
|
5
|
+
element: FallbackDescriptor;
|
|
6
6
|
parent: string | null;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
9
|
export type FallbackRegistry = {
|
|
10
10
|
nodes: Map<string, Node>;
|
|
11
11
|
currentParent: string | null;
|
|
12
|
+
prebuild: Map<string, React.ReactElement | React.ComponentType<any>>;
|
|
12
13
|
};
|
|
13
14
|
|
|
14
|
-
export
|
|
15
|
+
export type FallbackDescriptor =
|
|
16
|
+
| string
|
|
17
|
+
| React.ReactElement
|
|
18
|
+
| React.ComponentType;
|
|
19
|
+
|
|
20
|
+
export const FallbackContext = React.createContext<FallbackRegistry | null>(
|
|
21
|
+
null,
|
|
22
|
+
);
|
package/src/utils/renderNode.ts
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import type { Node } from "../types/FallbackRegistry";
|
|
2
|
+
import type { FallbackRegistry, Node } from "../types/FallbackRegistry";
|
|
3
|
+
import { resolveElement } from "./resolveElement";
|
|
3
4
|
|
|
4
5
|
export function renderNode(
|
|
6
|
+
|
|
5
7
|
node: Node,
|
|
6
8
|
childMap: Map<string | null, Node[]>,
|
|
7
|
-
|
|
9
|
+
registry: FallbackRegistry,
|
|
10
|
+
): React.ReactElement | null {
|
|
8
11
|
const children = childMap.get(node.id) ?? [];
|
|
12
|
+
const resolvedEle = resolveElement(node.element, registry);
|
|
13
|
+
if (!resolvedEle) return null;
|
|
9
14
|
|
|
10
15
|
return React.cloneElement(
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
children.map((child) => renderNode(child, childMap)),
|
|
16
|
+
resolvedEle,
|
|
17
|
+
{ key: node.id },
|
|
18
|
+
children.map((child) => renderNode(child, childMap, registry)),
|
|
14
19
|
);
|
|
15
20
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import {
|
|
3
|
+
FallbackDescriptor,
|
|
4
|
+
FallbackRegistry,
|
|
5
|
+
} from "../types/FallbackRegistry";
|
|
6
|
+
|
|
7
|
+
export function resolveElement(
|
|
8
|
+
descriptor: FallbackDescriptor,
|
|
9
|
+
registry: FallbackRegistry,
|
|
10
|
+
): React.ReactElement | null {
|
|
11
|
+
if (React.isValidElement(descriptor)) {
|
|
12
|
+
return descriptor;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (typeof descriptor === "function") {
|
|
16
|
+
return React.createElement(descriptor);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (typeof descriptor === "string") {
|
|
20
|
+
const value = registry.prebuild.get(descriptor);
|
|
21
|
+
if (!value) return null;
|
|
22
|
+
|
|
23
|
+
if (React.isValidElement(value)) {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const Component = value as React.ComponentType<any>;
|
|
28
|
+
return React.createElement(Component);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return null;
|
|
32
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2020",
|
|
4
|
-
"module": "
|
|
5
|
-
"
|
|
6
|
-
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"lib": ["DOM", "ES2020"],
|
|
7
9
|
"types": ["react"],
|
|
8
10
|
|
|
9
11
|
"declaration": true,
|
|
@@ -14,8 +16,6 @@
|
|
|
14
16
|
"rootDir": "./src",
|
|
15
17
|
|
|
16
18
|
"strict": true,
|
|
17
|
-
|
|
18
|
-
"moduleResolution": "Bundler",
|
|
19
19
|
"esModuleInterop": true,
|
|
20
20
|
"skipLibCheck": true
|
|
21
21
|
},
|
|
Binary file
|