@tempots/ui 16.0.10 → 16.1.1
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 +21 -26
- package/index.cjs +1 -1
- package/index.js +321 -299
- package/package.json +2 -2
- package/renderables/mutation.d.ts +18 -13
- package/renderables/query.d.ts +28 -63
- package/utils/mutation-resource.d.ts +22 -10
- package/utils/query-resource.d.ts +2 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tempots/ui",
|
|
3
|
-
"version": "16.
|
|
3
|
+
"version": "16.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "./index.cjs",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"license": "Apache-2.0",
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"@floating-ui/dom": "^1.7.1",
|
|
43
|
-
"@tempots/dom": "^37.
|
|
43
|
+
"@tempots/dom": "^37.5.0",
|
|
44
44
|
"@tempots/std": "^0.29.1"
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Renderable, Signal, TNode } from '@tempots/dom';
|
|
2
2
|
import { AsyncResult, NonLoading } from '@tempots/std';
|
|
3
|
-
import { MutationResource,
|
|
3
|
+
import { MutationResource, MutationResourceLoadOptions } from '../utils/mutation-resource';
|
|
4
4
|
export interface MutationContentOptions<Req, Res, E> {
|
|
5
5
|
previous: Signal<Res | undefined>;
|
|
6
6
|
execute: (request: Req) => void;
|
|
@@ -18,7 +18,7 @@ export interface MutationContentOptions<Req, Res, E> {
|
|
|
18
18
|
* @public
|
|
19
19
|
*/
|
|
20
20
|
export interface MutationDisplayOptions<Req, Res, E> {
|
|
21
|
-
/** Function to render
|
|
21
|
+
/** Function to render the mutation content. */
|
|
22
22
|
content: (options: MutationContentOptions<Req, Res, E>) => TNode;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
@@ -35,23 +35,28 @@ export interface MutationDisplayOptions<Req, Res, E> {
|
|
|
35
35
|
*/
|
|
36
36
|
export declare const MutationDisplay: <Req, Res, E>(resource: MutationResource<Req, Res, E>, options: MutationDisplayOptions<Req, Res, E>) => Renderable;
|
|
37
37
|
/**
|
|
38
|
-
* Creates a reactive mutation component for handling asynchronous
|
|
38
|
+
* Creates a reactive mutation component for handling asynchronous write operations.
|
|
39
39
|
*
|
|
40
|
-
* This component provides a declarative way to handle
|
|
41
|
-
* loading, success, and error states.
|
|
42
|
-
*
|
|
40
|
+
* This component provides a declarative way to handle mutations (POST/PUT/PATCH/DELETE)
|
|
41
|
+
* with proper loading, success, and error states. Unlike Query, mutations are triggered
|
|
42
|
+
* explicitly via `execute()` rather than reactively from a request signal.
|
|
43
43
|
*
|
|
44
|
-
* @typeParam Req - The type of the request.
|
|
45
|
-
* @typeParam Res - The type of the value when the
|
|
46
|
-
* @typeParam E - The type of the error when the
|
|
44
|
+
* @typeParam Req - The type of the request payload.
|
|
45
|
+
* @typeParam Res - The type of the value when the mutation succeeds.
|
|
46
|
+
* @typeParam E - The type of the error when the mutation fails.
|
|
47
47
|
*
|
|
48
|
-
* @param
|
|
49
|
-
* @param
|
|
50
|
-
* @
|
|
48
|
+
* @param options - Configuration object for the mutation
|
|
49
|
+
* @param options.mutate - Async function that performs the mutation
|
|
50
|
+
* @param options.convertError - Optional function to transform errors into a specific type
|
|
51
|
+
* @param options.onSuccess - Optional callback for successful mutations
|
|
52
|
+
* @param options.onError - Optional callback for failed mutations
|
|
53
|
+
* @param options.onSettled - Optional callback for both successful and failed mutations
|
|
54
|
+
* @param options.content - Function to render the mutation UI
|
|
55
|
+
* @returns A renderable component
|
|
51
56
|
* @public
|
|
52
57
|
*/
|
|
53
58
|
export declare const Mutation: <Req, Res, E = unknown>({ mutate, convertError, onSuccess, onError, onSettled, content, }: {
|
|
54
|
-
mutate: (options:
|
|
59
|
+
mutate: (options: MutationResourceLoadOptions<Req, Res, E>) => Promise<Res>;
|
|
55
60
|
convertError?: (error: unknown) => E;
|
|
56
61
|
onSuccess?: (value: Res, req: Req) => void;
|
|
57
62
|
onError?: (error: E, req: Req) => void;
|
package/renderables/query.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ import { AsyncResult, NonLoading } from '@tempots/std';
|
|
|
9
9
|
* @public
|
|
10
10
|
*/
|
|
11
11
|
export interface QueryDisplayOptions<Res, E> {
|
|
12
|
+
/** Function to render when the query has not been requested yet. */
|
|
13
|
+
notAsked?: () => TNode;
|
|
12
14
|
/** Function to render when the query is loading. */
|
|
13
15
|
pending?: (options: {
|
|
14
16
|
previous: Signal<Res | undefined>;
|
|
@@ -24,7 +26,15 @@ export interface QueryDisplayOptions<Res, E> {
|
|
|
24
26
|
success: (options: {
|
|
25
27
|
value: Signal<Res>;
|
|
26
28
|
reload: () => void;
|
|
29
|
+
loading: Signal<boolean>;
|
|
27
30
|
}) => TNode;
|
|
31
|
+
/**
|
|
32
|
+
* When true, the success view stays mounted during reloads instead of
|
|
33
|
+
* switching to the pending view. The `loading` signal passed to `success`
|
|
34
|
+
* indicates when a reload is in progress. Only applies when the query
|
|
35
|
+
* was already in a success state before the reload.
|
|
36
|
+
*/
|
|
37
|
+
keepOnReload?: boolean;
|
|
28
38
|
}
|
|
29
39
|
/**
|
|
30
40
|
* Component to display an asynchronous query based on its current status.
|
|
@@ -57,90 +67,43 @@ export declare const QueryDisplay: <Res, E>(query: QueryResource<Res, E>, option
|
|
|
57
67
|
* if (!response.ok) throw new Error('Failed to load user')
|
|
58
68
|
* return response.json()
|
|
59
69
|
* },
|
|
60
|
-
*
|
|
61
|
-
* failure: (error, reload) => html.div(
|
|
70
|
+
* pending: () => html.div('Loading user...'),
|
|
71
|
+
* failure: ({ error, reload }) => html.div(
|
|
62
72
|
* 'Error: ', error,
|
|
63
73
|
* html.button(on.click(reload), 'Retry')
|
|
64
74
|
* ),
|
|
65
|
-
* success: (
|
|
66
|
-
* html.h2(
|
|
67
|
-
* html.p(
|
|
75
|
+
* success: ({ value }) => html.div(
|
|
76
|
+
* html.h2(value.map(u => u.name)),
|
|
77
|
+
* html.p(value.map(u => u.email))
|
|
68
78
|
* )
|
|
69
79
|
* })
|
|
70
80
|
* ```
|
|
71
81
|
*
|
|
72
82
|
* @example
|
|
73
83
|
* ```typescript
|
|
74
|
-
* // Query with
|
|
84
|
+
* // Query with keepOnReload — success view stays mounted during reloads
|
|
75
85
|
* const searchQuery = prop('')
|
|
76
|
-
* const filters = prop({ category: 'all', sort: 'name' })
|
|
77
86
|
*
|
|
78
87
|
* const SearchResults = Query({
|
|
79
|
-
* request:
|
|
80
|
-
*
|
|
81
|
-
* ...filters.value
|
|
82
|
-
* })),
|
|
88
|
+
* request: searchQuery,
|
|
89
|
+
* keepOnReload: true,
|
|
83
90
|
* load: async ({ request, abortSignal }) => {
|
|
84
|
-
* const
|
|
85
|
-
* const response = await fetch(`/api/search?${params}`, {
|
|
91
|
+
* const response = await fetch(`/api/search?q=${request}`, {
|
|
86
92
|
* signal: abortSignal
|
|
87
93
|
* })
|
|
88
94
|
* return response.json()
|
|
89
95
|
* },
|
|
90
96
|
* convertError: (error) => error instanceof Error ? error.message : 'Unknown error',
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* previous.value && html.div('Previous results:', previous.value.length)
|
|
94
|
-
* ),
|
|
95
|
-
* failure: (error, reload) => html.div(
|
|
97
|
+
* pending: () => html.div('Searching...'),
|
|
98
|
+
* failure: ({ error, reload }) => html.div(
|
|
96
99
|
* attr.class('error'),
|
|
97
100
|
* 'Search failed: ', error,
|
|
98
101
|
* html.button(on.click(reload), 'Try again')
|
|
99
102
|
* ),
|
|
100
|
-
* success: (
|
|
103
|
+
* success: ({ value, reload, loading }) => html.div(
|
|
101
104
|
* html.button(on.click(reload), 'Refresh'),
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* })
|
|
105
|
-
* ```
|
|
106
|
-
*
|
|
107
|
-
* @example
|
|
108
|
-
* ```typescript
|
|
109
|
-
* // File upload query
|
|
110
|
-
* const selectedFile = prop<File | null>(null)
|
|
111
|
-
*
|
|
112
|
-
* const FileUpload = Query({
|
|
113
|
-
* request: selectedFile,
|
|
114
|
-
* load: async ({ request }) => {
|
|
115
|
-
* if (!request) throw new Error('No file selected')
|
|
116
|
-
*
|
|
117
|
-
* const formData = new FormData()
|
|
118
|
-
* formData.append('file', request)
|
|
119
|
-
*
|
|
120
|
-
* const response = await fetch('/api/upload', {
|
|
121
|
-
* method: 'POST',
|
|
122
|
-
* body: formData
|
|
123
|
-
* })
|
|
124
|
-
*
|
|
125
|
-
* if (!response.ok) throw new Error('Upload failed')
|
|
126
|
-
* return response.json()
|
|
127
|
-
* },
|
|
128
|
-
* loading: () => html.div(
|
|
129
|
-
* attr.class('upload-progress'),
|
|
130
|
-
* 'Uploading file...'
|
|
131
|
-
* ),
|
|
132
|
-
* failure: (error, reload) => html.div(
|
|
133
|
-
* attr.class('upload-error'),
|
|
134
|
-
* 'Upload failed: ', error,
|
|
135
|
-
* html.button(on.click(reload), 'Retry upload')
|
|
136
|
-
* ),
|
|
137
|
-
* success: (result) => html.div(
|
|
138
|
-
* attr.class('upload-success'),
|
|
139
|
-
* 'File uploaded successfully!',
|
|
140
|
-
* html.a(
|
|
141
|
-
* attr.href(result.map(r => r.url)),
|
|
142
|
-
* 'View file'
|
|
143
|
-
* )
|
|
105
|
+
* loading.map(l => l ? html.div('Refreshing...') : null),
|
|
106
|
+
* ForEach(value, result => SearchResultItem(result))
|
|
144
107
|
* )
|
|
145
108
|
* })
|
|
146
109
|
* ```
|
|
@@ -155,13 +118,15 @@ export declare const QueryDisplay: <Res, E>(query: QueryResource<Res, E>, option
|
|
|
155
118
|
* @param options.onSuccess - Optional callback for successful loads
|
|
156
119
|
* @param options.onError - Optional callback for failed loads
|
|
157
120
|
* @param options.onSettled - Optional callback for both successful and failed loads
|
|
121
|
+
* @param options.notAsked - Optional function to render before the first load
|
|
158
122
|
* @param options.success - Function to render when the query has successfully loaded
|
|
159
123
|
* @param options.pending - Optional function to render when the query is loading
|
|
160
124
|
* @param options.failure - Optional function to render when the query has failed to load
|
|
161
|
-
* @
|
|
125
|
+
* @param options.keepOnReload - When true, keeps the success view mounted during reloads
|
|
126
|
+
* @returns A renderable component
|
|
162
127
|
* @public
|
|
163
128
|
*/
|
|
164
|
-
export declare const Query: <Req, Res, E = unknown>({ request, load, convertError, onSuccess, onError, onSettled, success, pending, failure, }: {
|
|
129
|
+
export declare const Query: <Req, Res, E = unknown>({ request, load, convertError, onSuccess, onError, onSettled, notAsked, success, pending, failure, keepOnReload, }: {
|
|
165
130
|
request: Value<Req>;
|
|
166
131
|
load: (options: QueryResourceLoadOptions<Req, Res, E>) => Promise<Res>;
|
|
167
132
|
convertError?: (error: unknown) => E;
|
|
@@ -18,23 +18,39 @@ export interface MutationResource<Req, Res, E> {
|
|
|
18
18
|
/** Whether a mutation is currently in flight. */
|
|
19
19
|
readonly pending: Signal<boolean>;
|
|
20
20
|
/** Execute the mutation. */
|
|
21
|
-
readonly execute: (request: Req, options?: MutationResourceExecuteOptions<Req, Res
|
|
21
|
+
readonly execute: (request: Req, options?: MutationResourceExecuteOptions<Req, Res>) => void;
|
|
22
22
|
/** Abort the current in-flight request (if any) and clean up. */
|
|
23
23
|
readonly cancel: (newState?: NonLoading<Res, E>) => void;
|
|
24
24
|
/** Dispose of resources, aborting any in-flight request. */
|
|
25
25
|
readonly dispose: () => void;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* Options passed to the mutate function.
|
|
29
|
+
*
|
|
30
|
+
* @typeParam Req - The request payload/type.
|
|
31
|
+
* @typeParam Res - The response/value type on success.
|
|
32
|
+
* @typeParam E - The error type on failure.
|
|
33
|
+
* @public
|
|
30
34
|
*/
|
|
31
|
-
export interface
|
|
35
|
+
export interface MutationResourceLoadOptions<Req, Res, E> {
|
|
32
36
|
/** The request to execute the mutation. */
|
|
33
37
|
readonly request: Req;
|
|
34
|
-
/**
|
|
38
|
+
/** Abort signal for this execution. */
|
|
35
39
|
readonly abortSignal: AbortSignal;
|
|
36
40
|
/** The previous result of the mutation, if any. */
|
|
37
41
|
readonly previous: AsyncResult<Res, E>;
|
|
42
|
+
/** Abort the current in-flight request and optionally set a new state. */
|
|
43
|
+
readonly cancel: (newState?: NonLoading<Res, E>) => void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Caller-controlled options for execute().
|
|
47
|
+
* Useful for optimistic UI.
|
|
48
|
+
*
|
|
49
|
+
* @typeParam Req - The request payload/type.
|
|
50
|
+
* @typeParam Res - The response/value type on success.
|
|
51
|
+
* @public
|
|
52
|
+
*/
|
|
53
|
+
export interface MutationResourceExecuteOptions<Req, Res> {
|
|
38
54
|
/**
|
|
39
55
|
* Optionally provide an optimistic value to set immediately.
|
|
40
56
|
* This will set status to Loading with value prefilled.
|
|
@@ -45,13 +61,9 @@ export interface MutationResourceExecuteOptions<Req, Res, E> {
|
|
|
45
61
|
* Runs only if optimisticValue is not provided.
|
|
46
62
|
*/
|
|
47
63
|
readonly optimisticFromRequest?: (req: Req) => Res;
|
|
48
|
-
/** Side-effects */
|
|
49
|
-
readonly onSuccess?: (value: Res, req: Req) => void;
|
|
50
|
-
readonly onError?: (error: E, req: Req) => void;
|
|
51
|
-
readonly onSettled?: (result: AsyncResult<Res, E>, req: Req) => void;
|
|
52
64
|
}
|
|
53
65
|
export declare const makeMutationResource: <Req, Res, E>({ mutate, convertError, onSuccess, onError, onSettled, }: {
|
|
54
|
-
mutate: (options:
|
|
66
|
+
mutate: (options: MutationResourceLoadOptions<Req, Res, E>) => Promise<Res>;
|
|
55
67
|
convertError: (error: unknown) => E;
|
|
56
68
|
onSuccess?: (value: Res, req: Req) => void;
|
|
57
69
|
onError?: (error: E, req: Req) => void;
|
|
@@ -39,10 +39,8 @@ export interface QueryResourceLoadOptions<Req, Res, E> {
|
|
|
39
39
|
readonly abortSignal: AbortSignal;
|
|
40
40
|
/** The previous result of the query loading, if any. */
|
|
41
41
|
readonly previous: AsyncResult<Res, E>;
|
|
42
|
-
/**
|
|
43
|
-
readonly
|
|
44
|
-
readonly onError?: (error: E, req: Req) => void;
|
|
45
|
-
readonly onSettled?: (result: AsyncResult<Res, E>, req: Req) => void;
|
|
42
|
+
/** Abort the current in-flight request and optionally set a new state. */
|
|
43
|
+
readonly cancel: (newState?: NonLoading<Res, E>) => void;
|
|
46
44
|
}
|
|
47
45
|
/**
|
|
48
46
|
* Creates an asynchronous query that can be loaded, reloaded, and disposed of.
|