@tempots/ui
Version:
Provides a higher level of renderables to help fast development with Tempo.
172 lines (171 loc) • 6.29 kB
TypeScript
import { Renderable, Signal, TNode, Value } from '@tempots/dom';
import { QueryResource, QueryResourceLoadOptions } from '../utils/query-resource';
import { AsyncResult, NonLoading } from '@tempots/std';
/**
* Options for displaying the different states of an asynchronous query.
*
* @template Res - The type of the value when the query is successfully loaded.
* @template E - The type of the error when the query fails to load.
* @public
*/
export interface QueryDisplayOptions<Res, E> {
/** Function to render when the query is loading. */
pending?: (options: {
previous: Signal<Res | undefined>;
reload: () => void;
cancel: (newState?: NonLoading<Res, E>) => void;
}) => TNode;
/** Function to render when the query has failed to load. */
failure?: (options: {
error: Signal<E>;
reload: () => void;
}) => TNode;
/** Function to render when the query has successfully loaded. */
success: (options: {
value: Signal<Res>;
reload: () => void;
}) => TNode;
}
/**
* Component to display an asynchronous query based on its current status.
*
* @template Res - The type of the value when the query is successfully loaded.
* @template E - The type of the error when the query fails to load.
*
* @param {QueryResource<Res, E>} query - The asynchronous query to display.
* @param {QueryDisplayOptions<Res, E>} options - The display options for the query.
* @returns {TNode} A node representing the current state of the query.
* @public
*/
export declare const QueryDisplay: <Res, E>(query: QueryResource<Res, E>, options: QueryDisplayOptions<Res, E>) => Renderable;
/**
* Creates a reactive query component for handling asynchronous data loading.
*
* This component provides a declarative way to handle async operations with proper
* loading, success, and error states. It automatically manages the lifecycle of
* async requests and provides reload functionality.
*
* @example
* ```typescript
* // Basic API data loading
* const userId = prop(1)
*
* const UserProfile = Query({
* request: userId,
* load: async ({ request }) => {
* const response = await fetch(`/api/users/${request}`)
* if (!response.ok) throw new Error('Failed to load user')
* return response.json()
* },
* loading: () => html.div('Loading user...'),
* failure: (error, reload) => html.div(
* 'Error: ', error,
* html.button(on.click(reload), 'Retry')
* ),
* success: (user) => html.div(
* html.h2(user.map(u => u.name)),
* html.p(user.map(u => u.email))
* )
* })
* ```
*
* @example
* ```typescript
* // Query with dependencies
* const searchQuery = prop('')
* const filters = prop({ category: 'all', sort: 'name' })
*
* const SearchResults = Query({
* request: computed(() => ({
* query: searchQuery.value,
* ...filters.value
* })),
* load: async ({ request, abortSignal }) => {
* const params = new URLSearchParams(request)
* const response = await fetch(`/api/search?${params}`, {
* signal: abortSignal
* })
* return response.json()
* },
* convertError: (error) => error instanceof Error ? error.message : 'Unknown error',
* loading: (previous) => html.div(
* 'Searching...',
* previous.value && html.div('Previous results:', previous.value.length)
* ),
* failure: (error, reload) => html.div(
* attr.class('error'),
* 'Search failed: ', error,
* html.button(on.click(reload), 'Try again')
* ),
* success: (results, reload) => html.div(
* html.button(on.click(reload), 'Refresh'),
* ForEach(results, result => SearchResultItem(result))
* )
* })
* ```
*
* @example
* ```typescript
* // File upload query
* const selectedFile = prop<File | null>(null)
*
* const FileUpload = Query({
* request: selectedFile,
* load: async ({ request }) => {
* if (!request) throw new Error('No file selected')
*
* const formData = new FormData()
* formData.append('file', request)
*
* const response = await fetch('/api/upload', {
* method: 'POST',
* body: formData
* })
*
* if (!response.ok) throw new Error('Upload failed')
* return response.json()
* },
* loading: () => html.div(
* attr.class('upload-progress'),
* 'Uploading file...'
* ),
* failure: (error, reload) => html.div(
* attr.class('upload-error'),
* 'Upload failed: ', error,
* html.button(on.click(reload), 'Retry upload')
* ),
* success: (result) => html.div(
* attr.class('upload-success'),
* 'File uploaded successfully!',
* html.a(
* attr.href(result.map(r => r.url)),
* 'View file'
* )
* )
* })
* ```
*
* @template Req - The type of the request parameter
* @template Res - The type of the successful result value
* @template E - The type of the error (defaults to unknown)
* @param options - Configuration object for the query
* @param options.request - Signal or value representing the request parameters
* @param options.load - Async function that loads the query
* @param options.convertError - Optional function to transform errors into a specific type
* @param options.onSuccess - Optional callback for successful loads
* @param options.onError - Optional callback for failed loads
* @param options.onSettled - Optional callback for both successful and failed loads
* @param options.success - Function to render when the query has successfully loaded
* @param options.pending - Optional function to render when the query is loading
* @param options.failure - Optional function to render when the query has failed to load
* @returns Function that takes display options and returns a renderable component
* @public
*/
export declare const Query: <Req, Res, E = unknown>({ request, load, convertError, onSuccess, onError, onSettled, success, pending, failure, }: {
request: Value<Req>;
load: (options: QueryResourceLoadOptions<Req, Res, E>) => Promise<Res>;
convertError?: (error: unknown) => E;
onSuccess?: (value: Res, req: Req) => void;
onError?: (error: E, req: Req) => void;
onSettled?: (result: AsyncResult<Res, E>, req: Req) => void;
} & QueryDisplayOptions<Res, E>) => Renderable;