@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tempots/ui",
3
- "version": "16.0.10",
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.4.3",
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, MutationResourceExecuteOptions } from '../utils/mutation-resource';
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 when the query is loading. */
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 data loading.
38
+ * Creates a reactive mutation component for handling asynchronous write operations.
39
39
  *
40
- * This component provides a declarative way to handle async operations with proper
41
- * loading, success, and error states. It automatically manages the lifecycle of
42
- * async requests and provides reload functionality.
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 resource is successfully loaded.
46
- * @typeParam E - The type of the error when the resource fails to load.
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 {MutationResource<Req, Res, E>} resource - The asynchronous resource to display.
49
- * @param {MutationDisplayOptions<Req, Res, E>} options - The display options for the resource.
50
- * @returns {Renderable} A node representing the current state of the resource.
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: MutationResourceExecuteOptions<Req, Res, E>) => Promise<Res>;
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;
@@ -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
- * loading: () => html.div('Loading user...'),
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: (user) => html.div(
66
- * html.h2(user.map(u => u.name)),
67
- * html.p(user.map(u => u.email))
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 dependencies
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: computed(() => ({
80
- * query: searchQuery.value,
81
- * ...filters.value
82
- * })),
88
+ * request: searchQuery,
89
+ * keepOnReload: true,
83
90
  * load: async ({ request, abortSignal }) => {
84
- * const params = new URLSearchParams(request)
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
- * loading: (previous) => html.div(
92
- * 'Searching...',
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: (results, reload) => html.div(
103
+ * success: ({ value, reload, loading }) => html.div(
101
104
  * html.button(on.click(reload), 'Refresh'),
102
- * ForEach(results, result => SearchResultItem(result))
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
- * @returns Function that takes display options and returns a renderable component
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, E>) => void;
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
- * Execution-time options for a mutation.
29
- * Useful for optimistic UI and side-effects.
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 MutationResourceExecuteOptions<Req, Res, E> {
35
+ export interface MutationResourceLoadOptions<Req, Res, E> {
32
36
  /** The request to execute the mutation. */
33
37
  readonly request: Req;
34
- /** External abort signal for this single execution. */
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: MutationResourceExecuteOptions<Req, Res, E>) => Promise<Res>;
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
- /** Side-effects */
43
- readonly onSuccess?: (value: Res, req: Req) => void;
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.