@hybridly/core
Version:
Core functionality of Hybridly
544 lines (527 loc) • 21.3 kB
text/typescript
import { RequestData } from '@hybridly/utils';
import { AxiosResponse, AxiosProgressEvent, Axios } from 'axios';
type MaybePromise<T> = T | Promise<T>;
interface RequestHooks {
/**
* Called before a navigation request is going to happen.
*/
before: (options: HybridRequestOptions, context: InternalRouterContext) => MaybePromise<any | boolean>;
/**
* Called before the request of a navigation is going to happen.
*/
start: (context: InternalRouterContext) => MaybePromise<any>;
/**
* Called when progress on the request is being made.
*/
progress: (progress: Progress, context: InternalRouterContext) => MaybePromise<any>;
/**
* Called when data is received after a request for a navigation.
*/
data: (response: AxiosResponse, context: InternalRouterContext) => MaybePromise<any>;
/**
* Called when a request is successful and there is no error.
*/
success: (payload: HybridPayload, context: InternalRouterContext) => MaybePromise<any>;
/**
* Called when a request is successful but there were errors.
*/
error: (errors: Errors, context: InternalRouterContext) => MaybePromise<any>;
/**
* Called when a request has been aborted.
*/
abort: (context: InternalRouterContext) => MaybePromise<any>;
/**
* Called when a response to a request is not a valid hybrid response.
*/
invalid: (response: AxiosResponse, context: InternalRouterContext) => MaybePromise<any>;
/**
* Called when an unknowne exception was triggered.
*/
exception: (error: Error, context: InternalRouterContext) => MaybePromise<any>;
/**
* Called whenever the request failed, for any reason, in addition to other hooks.
*/
fail: (context: InternalRouterContext) => MaybePromise<any>;
/**
* Called after a request has been made, even if it didn't succeed.
*/
after: (context: InternalRouterContext) => MaybePromise<any>;
}
interface Hooks extends RequestHooks {
/**
/////////////////////// * Called when Hybridly's context is initialized.
///////////////////////
///////////////////////
///////////////////////
///////////////////////
///////////////////////
///////////////////////
///////////////////////
///////////////////////
///////////////////////
///////////////////////
*/
initialized: (context: InternalRouterContext) => MaybePromise<any>;
/**
* Called after Hybridly's initial load.
*/
ready: (context: InternalRouterContext) => MaybePromise<any>;
/**
* Called when a back-forward navigation occurs.
*/
backForward: (state: any, context: InternalRouterContext) => MaybePromise<any>;
/**
* Called when a component navigation is being made.
*/
navigating: (options: InternalNavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
/**
* Called when a component has been navigated to.
*/
navigated: (options: InternalNavigationOptions, context: InternalRouterContext) => MaybePromise<any>;
/**
* Called when a component has been navigated to and was mounted by the adapter.
*/
mounted: (options: InternalNavigationOptions & MountedHookOptions, context: InternalRouterContext) => MaybePromise<any>;
}
interface MountedHookOptions {
/**
* Whether the component being mounted is a dialog.
*/
isDialog: boolean;
}
interface HookOptions {
/** Executes the hook only once. */
once?: boolean;
}
/**
* Registers a global hook.
*/
declare function registerHook<T extends keyof Hooks>(hook: T, fn: Hooks[T], options?: HookOptions): () => void;
interface CloseDialogOptions extends HybridRequestOptions {
/**
* Close the dialog without a round-trip to the server.
* @default false
*/
local?: boolean;
}
interface RoutingConfiguration {
url: string;
port?: number;
defaults: Record<string, any>;
routes: Record<string, RouteDefinition>;
}
interface RouteDefinition {
uri: string;
method: Method[];
bindings: Record<string, string>;
domain?: string;
wheres?: Record<string, string>;
name: string;
}
interface GlobalRouteCollection extends RoutingConfiguration {
}
type RouteName = keyof GlobalRouteCollection['routes'];
type RouteParameters<T extends RouteName> = Record<keyof GlobalRouteCollection['routes'][T]['bindings'], any> & Record<string, any>;
type UrlResolvable = string | URL | Location;
type UrlTransformable = BaseUrlTransformable | ((string: URL) => BaseUrlTransformable);
type BaseUrlTransformable = Partial<Omit<URL, 'searchParams' | 'toJSON' | 'toString'>> & {
query?: any;
trailingSlash?: boolean;
};
/**
* Converts an input to an URL, optionally changing its properties after initialization.
*/
declare function makeUrl(href: UrlResolvable, transformations?: UrlTransformable): URL;
/**
* Checks if the given URLs have the same origin and path.
*/
declare function sameUrls(...hrefs: UrlResolvable[]): boolean;
type ConditionalNavigationOption<T extends boolean | string> = T | ((payload: NavigationOptions) => T);
interface ComponentNavigationOptions {
/** Dialog data. */
dialog?: Dialog | false;
/** Name of the component to use. */
component?: string;
/** Properties to apply to the component. */
properties?: Properties;
/**
* Whether to replace the current history state instead of adding
* one. This affects the browser's "back" and "forward" features.
*/
replace?: ConditionalNavigationOption<boolean>;
/** Whether to preserve the current scrollbar position. */
preserveScroll?: ConditionalNavigationOption<boolean>;
/** Whether to preserve the current view component state. */
preserveState?: ConditionalNavigationOption<boolean>;
}
interface NavigationOptions {
/** View to navigate to. */
payload?: HybridPayload;
/**
* Whether to replace the current history state instead of adding
* one. This affects the browser's "back" and "forward" features.
*/
replace?: ConditionalNavigationOption<boolean>;
/** Whether to preserve the scrollbars positions on the view. */
preserveScroll?: ConditionalNavigationOption<boolean>;
/** Whether to preserve the current view component's state. */
preserveState?: ConditionalNavigationOption<boolean>;
/** Whether to preserve the current URL. */
preserveUrl?: ConditionalNavigationOption<boolean>;
/**
* Properties of the given URL to override.
* @example
* ```ts
* router.get('/login?redirect=/', {
* transformUrl: { search: '' }
* }
* ```
*/
transformUrl?: UrlTransformable;
/**
* Defines whether the history state should be updated.
* @internal
*/
updateHistoryState?: boolean;
}
interface InternalNavigationOptions extends NavigationOptions {
/**
* Defines the kind of navigation being performed.
* - initial: the initial load's navigation
* - server: a navigation initiated by a server round-trip
* - local: a navigation initiated by `router.local`
* - back-forward: a navigation initiated by the browser's `popstate` event
* @internal
*/
type: 'initial' | 'local' | 'back-forward' | 'server';
/**
* Defines whether this navigation opens a dialog.
* @internal
*/
hasDialog?: boolean;
}
type Method = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
interface HybridRequestOptions extends Omit<NavigationOptions, 'payload'> {
/** The URL to navigation. */
url?: UrlResolvable;
/** HTTP verb to use for the request. */
method?: Method | Lowercase<Method>;
/** Body of the request. */
data?: RequestData;
/** Which properties to update for this navigation. Other properties will be ignored. */
only?: string | string[];
/** Which properties not to update for this navigation. Other properties will be updated. */
except?: string | string[];
/** Specific headers to add to the request. */
headers?: Record<string, string>;
/** The bag in which to put potential errors. */
errorBag?: string;
/** Hooks for this navigation. */
hooks?: Partial<RequestHooks>;
/** If `true`, force the usage of a `FormData` object. */
useFormData?: boolean;
/**
* If `false`, disable automatic form spoofing.
* @see https://laravel.com/docs/master/routing#form-method-spoofing
*/
spoof?: boolean;
/**
* If `false`, does not trigger the progress bar for this request.
*/
progress?: boolean;
}
interface NavigationResponse {
response?: AxiosResponse;
error?: {
type: string;
actual: Error;
};
}
interface DialogRouter {
/** Closes the current dialog. */
close: (options?: CloseDialogOptions) => void;
}
interface Router {
/** Aborts the currently pending navigate, if any. */
abort: () => Promise<void>;
/** Checks if there is an active navigate. */
active: () => boolean;
/** Makes a navigate with the given options. */
navigate: (options: HybridRequestOptions) => Promise<NavigationResponse>;
/** Reloads the current page. */
reload: (options?: HybridRequestOptions) => Promise<NavigationResponse>;
/** Makes a request to given named route. The HTTP verb is determined automatically but can be overriden. */
to: <T extends RouteName>(name: T, parameters?: RouteParameters<T>, options?: Omit<HybridRequestOptions, 'url'>) => Promise<NavigationResponse>;
/** Makes a GET request to the given URL. */
get: (url: UrlResolvable, options?: Omit<HybridRequestOptions, 'method' | 'url'>) => Promise<NavigationResponse>;
/** Makes a POST request to the given URL. */
post: (url: UrlResolvable, options?: Omit<HybridRequestOptions, 'method' | 'url'>) => Promise<NavigationResponse>;
/** Makes a PUT request to the given URL. */
put: (url: UrlResolvable, options?: Omit<HybridRequestOptions, 'method' | 'url'>) => Promise<NavigationResponse>;
/** Makes a PATCH request to the given URL. */
patch: (url: UrlResolvable, options?: Omit<HybridRequestOptions, 'method' | 'url'>) => Promise<NavigationResponse>;
/** Makes a DELETE request to the given URL. */
delete: (url: UrlResolvable, options?: Omit<HybridRequestOptions, 'method' | 'url'>) => Promise<NavigationResponse>;
/** Navigates to the given external URL. Convenience method using `document.location.href`. */
external: (url: UrlResolvable, data?: HybridRequestOptions['data']) => void;
/** Navigates to the given URL without a server round-trip. */
local: (url: UrlResolvable, options: ComponentNavigationOptions) => Promise<void>;
/** Preloads the given URL. The next time this URL is navigated to, it will be loaded from the cache. */
preload: (url: UrlResolvable, options?: Omit<HybridRequestOptions, 'method' | 'url'>) => Promise<boolean>;
/** Determines if the given route name and parameters matches the current route. */
matches: <T extends RouteName>(name: T, parameters?: RouteParameters<T>) => boolean;
/** Gets the current route name. Returns `undefined` is unknown. */
current: () => RouteName | undefined;
/** Access the dialog router. */
dialog: DialogRouter;
/** Access the history state. */
history: {
/** Remembers a value for the given route. */
remember: (key: string, value: any) => void;
/** Gets a remembered value. */
get: <T = any>(key: string) => T | undefined;
};
}
/** A navigation being made. */
interface PendingNavigation {
/** The URL to which the request is being made. */
url: URL;
/** Abort controller associated to this request. */
controller: AbortController;
/** Options for the associated hybrid request. */
options: HybridRequestOptions;
/** Navigation identifier. */
id: string;
/** Current status. */
status: 'pending' | 'success' | 'error';
}
/** A view or dialog component. */
interface View {
/** Name of the component to use. */
component?: string;
/** Properties to apply to the component. */
properties: Properties;
/** Deferred properties for this view. */
deferred: string[];
}
interface Dialog extends Required<View> {
/** URL that is the base background view when navigating to the dialog directly. */
baseUrl: string;
/** URL to which the dialog should redirect when closed. */
redirectUrl: string;
/** Unique identifier for this modal's lifecycle. */
key: string;
}
type Property = null | string | number | boolean | Property[] | {
[name: string]: Property;
};
type Properties = Record<string | number, Property>;
interface SwapOptions<T> {
/** The new component. */
component: T;
/** The new properties. */
properties?: any;
/** Whether to preserve the state of the component. */
preserveState?: boolean;
/** Current dialog. */
dialog?: Dialog;
/** On mounted callback. */
onMounted?: (options: MountedHookOptions) => void;
}
type ViewComponent = any;
type ResolveComponent = (name: string) => Promise<ViewComponent>;
type SwapView = (options: SwapOptions<ViewComponent>) => Promise<void>;
/** The payload of a navigation request from the server. */
interface HybridPayload {
/** The view to use in this request. */
view: View;
/** An optional dialog. */
dialog?: Dialog;
/** The current page URL. */
url: string;
/** The current asset version. */
version: string;
}
interface Progress {
/** Base event. */
event: AxiosProgressEvent;
/** Computed percentage. */
percentage: Readonly<number>;
}
type Errors = any;
interface Plugin extends Partial<Hooks> {
/** Identifier of the plugin. */
name: string;
}
declare function definePlugin(plugin: Plugin): Plugin;
/** Options for creating a router context. */
interface RouterContextOptions {
/** The initial payload served by the browser. */
payload: HybridPayload;
/** Adapter-specific functions. */
adapter: Adapter;
/** History state serializer. */
serializer?: Serializer;
/** List of plugins. */
plugins?: Plugin[];
/** The Axios instance. */
axios?: Axios;
/** Initial routing configuration. */
routing?: RoutingConfiguration;
/** Whether to display response error modals. */
responseErrorModals?: boolean;
}
/** Router context. */
interface InternalRouterContext {
/** The current, normalized URL. */
url: string;
/** The current view. */
view: View;
/** The current, optional dialog. */
dialog?: Dialog;
/** The current local asset version. */
version: string;
/** The current adapter's functions. */
adapter: ResolvedAdapter;
/** Scroll positions of the current page's DOM elements. */
scrollRegions: ScrollRegion[];
/** Arbitrary state. */
memo: Record<string, any>;
/** Currently pending navigation. */
pendingNavigation?: PendingNavigation;
/** History state serializer. */
serializer: Serializer;
/** List of plugins. */
plugins: Plugin[];
/** Global hooks. */
hooks: Partial<Record<keyof Hooks, Array<Function>>>;
/** The Axios instance. */
axios: Axios;
/** Routing configuration. */
routing?: RoutingConfiguration;
/** Whether to display response error modals. */
responseErrorModals?: boolean;
/** Cache of preload requests. */
preloadCache: Map<string, AxiosResponse>;
}
/** Router context. */
type RouterContext = Readonly<InternalRouterContext>;
/** Adapter-specific functions. */
interface Adapter {
/** Resolves a component from the given name. */
resolveComponent: ResolveComponent;
/** Called when the view is swapped. */
onViewSwap: SwapView;
/** Called when the context is updated. */
onContextUpdate?: (context: InternalRouterContext) => void;
/** Called when a dialog is closed. */
onDialogClose?: (context: InternalRouterContext) => void;
/** Called when Hybridly is waiting for a component to be mounted. The given callback should be executed after the view component is mounted. */
executeOnMounted: (callback: Function) => void;
}
interface ResolvedAdapter extends Adapter {
updateRoutingConfiguration: (routing?: RoutingConfiguration) => void;
}
interface ScrollRegion {
top: number;
left: number;
}
/** Provides methods to serialize the state into the history state. */
interface Serializer {
serialize: <T>(view: T) => string;
unserialize: <T>(state?: string) => T | undefined;
}
/** Gets the current context. */
declare function getRouterContext(): RouterContext;
/**
* The hybridly router.
* This is the core function that you can use to navigate in
* your application. Make sure the routes you call return a
* hybrid response, otherwise you need to call `external`.
*
* @example
* router.get('/posts/edit', { post })
*/
declare const router: Router;
/** Creates the hybridly router. */
declare function createRouter(options: RouterContextOptions): Promise<InternalRouterContext>;
interface Authorizable<Authorizations extends Record<string, boolean>> {
authorization: Authorizations;
}
/**
* Checks whether the given data has the authorization for the given action.
* If the data object has no authorization definition corresponding to the given action, this method will return `false`.
*/
declare function can<Authorizations extends Record<string, boolean>, Data extends Authorizable<Authorizations>, Action extends keyof Data['authorization']>(resource: Data, action: Action): Authorizations[Action];
/**
* Generates a route from the given route name.
*/
declare function route<T extends RouteName>(name: T, parameters?: RouteParameters<T>, absolute?: boolean): string;
interface DynamicConfiguration {
versions: {
composer: string;
npm: string;
latest: string;
is_latest: boolean;
};
architecture: {
root_directory: string;
components_directory: string;
application_main_path: string;
};
components: {
eager?: boolean;
files: string[];
views: Component[];
layouts: Component[];
components: Component[];
};
routing: RoutingConfiguration;
}
interface Component {
path: string;
identifier: string;
namespace: string;
}
declare const STORAGE_EXTERNAL_KEY = "hybridly:external";
declare const HYBRIDLY_HEADER = "x-hybrid";
declare const EXTERNAL_NAVIGATION_HEADER = "x-hybrid-external";
declare const EXTERNAL_NAVIGATION_TARGET_HEADER = "x-hybrid-external-target";
declare const PARTIAL_COMPONENT_HEADER = "x-hybrid-partial-component";
declare const ONLY_DATA_HEADER = "x-hybrid-only-data";
declare const DIALOG_KEY_HEADER = "x-hybrid-dialog-key";
declare const DIALOG_REDIRECT_HEADER = "x-hybrid-dialog-redirect";
declare const EXCEPT_DATA_HEADER = "x-hybrid-except-data";
declare const VERSION_HEADER = "x-hybrid-version";
declare const ERROR_BAG_HEADER = "x-hybrid-error-bag";
declare const SCROLL_REGION_ATTRIBUTE = "scroll-region";
declare const constants_DIALOG_KEY_HEADER: typeof DIALOG_KEY_HEADER;
declare const constants_DIALOG_REDIRECT_HEADER: typeof DIALOG_REDIRECT_HEADER;
declare const constants_ERROR_BAG_HEADER: typeof ERROR_BAG_HEADER;
declare const constants_EXCEPT_DATA_HEADER: typeof EXCEPT_DATA_HEADER;
declare const constants_EXTERNAL_NAVIGATION_HEADER: typeof EXTERNAL_NAVIGATION_HEADER;
declare const constants_EXTERNAL_NAVIGATION_TARGET_HEADER: typeof EXTERNAL_NAVIGATION_TARGET_HEADER;
declare const constants_HYBRIDLY_HEADER: typeof HYBRIDLY_HEADER;
declare const constants_ONLY_DATA_HEADER: typeof ONLY_DATA_HEADER;
declare const constants_PARTIAL_COMPONENT_HEADER: typeof PARTIAL_COMPONENT_HEADER;
declare const constants_SCROLL_REGION_ATTRIBUTE: typeof SCROLL_REGION_ATTRIBUTE;
declare const constants_STORAGE_EXTERNAL_KEY: typeof STORAGE_EXTERNAL_KEY;
declare const constants_VERSION_HEADER: typeof VERSION_HEADER;
declare namespace constants {
export {
constants_DIALOG_KEY_HEADER as DIALOG_KEY_HEADER,
constants_DIALOG_REDIRECT_HEADER as DIALOG_REDIRECT_HEADER,
constants_ERROR_BAG_HEADER as ERROR_BAG_HEADER,
constants_EXCEPT_DATA_HEADER as EXCEPT_DATA_HEADER,
constants_EXTERNAL_NAVIGATION_HEADER as EXTERNAL_NAVIGATION_HEADER,
constants_EXTERNAL_NAVIGATION_TARGET_HEADER as EXTERNAL_NAVIGATION_TARGET_HEADER,
constants_HYBRIDLY_HEADER as HYBRIDLY_HEADER,
constants_ONLY_DATA_HEADER as ONLY_DATA_HEADER,
constants_PARTIAL_COMPONENT_HEADER as PARTIAL_COMPONENT_HEADER,
constants_SCROLL_REGION_ATTRIBUTE as SCROLL_REGION_ATTRIBUTE,
constants_STORAGE_EXTERNAL_KEY as STORAGE_EXTERNAL_KEY,
constants_VERSION_HEADER as VERSION_HEADER,
};
}
export { can, constants, createRouter, definePlugin, getRouterContext, makeUrl, registerHook, route, router, sameUrls };
export type { Authorizable, DynamicConfiguration, GlobalRouteCollection, HybridPayload, HybridRequestOptions, MaybePromise, Method, NavigationResponse, Plugin, Progress, ResolveComponent, RouteDefinition, RouteName, RouteParameters, Router, RouterContext, RouterContextOptions, RoutingConfiguration, UrlResolvable };