--- url: /api/components/deferred.md --- # `` This headless component helps rendering [deferred properties](../../guide/partial-reloads.md#deferred-properties). It exposes loading state through slots so you can display placeholders until deferred data is available. ## Usage ```vue ``` ## Attributes ### `data` * **Required**: true * **Type**: `string | string[]` Defines the property key, or keys, to track. ## Slots ### `default` The default slot exposes the following properties: * `loading: boolean`: `true` when at least one tracked property is still missing. * `reloading: boolean`: `true` when tracked properties are being reloaded. * `loaded: boolean`: `true` when all tracked properties are loaded. Rendered when all tracked properties are loaded. ### `fallback` The `fallback` slots exposes the following properties: * `loading: boolean`: `true` when at least one tracked property is still missing. * `reloading: boolean`: `true` when tracked properties are being reloaded. * `loaded: boolean`: `true` when all tracked properties are loaded. Rendered while at least one tracked property is still missing. --- --- url: /api/components/form.md --- # `
` This component provides a native `` API powered by Hybridly's [`useForm`](../utils/use-form.md) composable. It reads fields from native controls (`name` attributes), submits using hybrid requests, and exposes the form state through its default slot. ## Usage ```vue ``` ## Attributes ### `action` * **Type**: `string` Defines the target URL. If omitted, it falls back to the underlying form `action` attribute, then to the current URL. ### `method` * **Type**: `GET`, `POST`, `PUT`, `PATCH` or `DELETE` * **Default**: `POST` Defines the HTTP method used when submitting. ### `options` * **Type**: `Omit` Defines request options forwarded to form submission. This is the same as passing options to [`submit`](../utils/use-form.md#submit). ### `errorBag` * **Type**: `string` Defines the validation error bag used by this form. ### `show-progress` * **Type**: `boolean` Defines whether request upload/download progress should be tracked. ### `disable-while-processing` * **Type**: `boolean` * **Default**: `false` When set to `true`, the native `inert` attribute is applied while the form is processing. ### `reset-on-success` * **Type**: `boolean` * **Default**: `true` Defines whether native controls should reset to defaults after a successful submission. ### `set-default-on-success` * **Type**: `boolean` * **Default**: `false` When set to `true`, current field values become the new default values after a successful submission. ## Default slot The default slot receives most of the [`useForm`](../utils/use-form.md) return values, plus additional helpers: * `errors`: nested validation errors object * `getError(key: string)`: reads nested errors with dot notation * `submit(options?)`: submits with optional overrides * `reset()`: clears submission flags and errors, then resets controls * `resetFields(...keys: string[])`: resets specific named fields or all fields ```vue ``` --- --- url: /api/components/router-link.md --- # `` This built-in component can be used to replace [anchor tags](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a) to navigate from a hybrid view to another. This component is a wrapper around Vue's [``](https://vuejs.org/api/built-in-special-elements.html#component). By default, it creates anchors elements but intercepts their click handlers to make [hybrid navigations](../../guide/navigation.md). ## Usage `` works the same as a normal anchor tag: ```vue ``` ## Attributes ### `href` * **Required**: true * **Type**: `string` Similar to the `` tag, accepts the hyperlink to navigate to. If this doesn't point to a hybrid view, set the [`external`](#external) property to `true`. ### `external` * **Type**: boolean When set to `true`, disables the custom click handler. This must be used when navigating to external websites or non-hybrid views — otherwise, a hybrid request will be made and will result into an error. #### Usage ```vue ``` ### `as` * **Type**: `string` or `Component` Defines the tag or component to render as. #### Usage ```vue ``` ### `method` * **Type**: `GET`, `POST`, `PUT`, `PATCH` or `DELETE` Defines the method that will be used when making the hybrid request. May be lowercase or uppercase. ### `mode` * **Type**: `'navigation' | 'async'` Defines whether the request is a full navigation or an asynchronous background request. ### `data` * **Type**: object Optional data to be sent with the hybrid request. This is the same as using `data` in a [programmatic navigation](../router/navigation.md). ### `options` * **Type**: `HybridRequestOptions` Options for the hybrid request. This is the same as the `options` argument in [programmatic navigations](../router/navigation.md). ### `disabled` * **Type**: boolean When set to `true`, the click handler will not be triggered and the `disabled` HTML attribute will be added. ### `preload` * **Type**: `boolean | 'mount' | 'hover'` Preloading has been removed from the router internals. The `preload` prop is currently a no-op and is kept only for backwards compatibility in templates. --- --- url: /api/components/when-visible.md --- # `` This headless component triggers a [partial reload](../../guide/partial-reloads.md) when it enters the viewport. Internally, this component uses [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) and makes a [`router.reload`](../router/navigation.md#reload) request with the configured [`data`](#data) as `only` keys. ## Usage ```vue ``` ## Attributes ### `data` * **Required**: true * **Type**: `string | string[]` Defines the property key, or keys, to reload when the component becomes visible. ### `as` * **Type**: `string` * **Default**: `'div'` Defines which HTML tag to render as the wrapper element. ### `buffer` * **Type**: `number` * **Default**: `0` Expands the observer margins by the given pixel value, so loading can start before the element is strictly visible. ### `root` * **Type**: `string | Element | null` * **Default**: `null` Defines the scroll root used by `IntersectionObserver`. You may pass a CSS selector string or a DOM element. ### `once` * **Type**: `boolean` * **Default**: `true` When `true`, the reload is only triggered once. When `false`, the component may reload again each time it re-enters the viewport. ### `fallbackOnHidden` * **Type**: `boolean` * **Default**: `false` When `false`, the default slot keeps rendering after data has loaded, even when the element leaves the viewport. When `true`, the `fallback` slot is shown again while hidden. ### `options` * **Type**: `Omit` Additional reload options forwarded to [`router.reload`](../router/navigation.md#reload). #### Usage ```vue ``` ## Slots ### `default` The default slot exposes the following properties: * `loading: boolean`: `true` when at least one tracked property is still missing. * `reloading: boolean`: `true` when tracked properties are being reloaded. * `loaded: boolean`: `true` when all tracked properties are loaded. Rendered when the data is loaded. ### `fallback` The `fallback` slots exposes the following properties: * `loading: boolean`: `true` when at least one tracked property is still missing. * `reloading: boolean`: `true` when tracked properties are being reloaded. * `loaded: boolean`: `true` when all tracked properties are loaded. Rendered while data is not loaded. ### `loading` The `loading` slots exposes the following properties: * `loading: boolean`: `true` when at least one tracked property is still missing. * `reloading: boolean`: `true` when tracked properties are being reloaded. * `loaded: boolean`: `true` when all tracked properties are loaded. Emitted right before the visibility-triggered reload starts. --- --- url: /api/utils/get-router-context.md --- # `getRouterContext` ## Usage ```ts interface RouterContext { /** The current, normalized URL. */ url: string /** The current component's name. */ view: View /** The current local asset version. */ version: string /** The current adapter's functions. */ adapter: Adapter /** Scroll positions of the current page's DOM elements. */ scrollRegions: ScrollRegion[] /** Arbitrary state. */ state: Record /** Currently pending navigation. */ pendingNavigation?: PendingNavigation /** History state serializer. */ serializer: Serializer /** List of plugins. */ plugins: Plugin[] /** Global hooks. */ hooks: Partial>> } function getRouterContext(): RouterContext ``` --- --- url: /api/utils/initialize-hybridly.md --- # `initializeHybridly` ## Example The following snippet is a typical example of how Hybridly could be set up. ```ts import { initializeHybridly } from 'virtual:hybridly/setup' import { createApp } from 'vue' initializeHybridly() ``` ## `enhanceVue` * **Type**: `(vue: App) => MaybePromise` Defines a callback that receives the Vue instance as a parameter and gets executed before Vue is mounted. This can be used to register additionnal Vue plugins, directives or components. ### Example ```ts import { autoAnimatePlugin as autoAnimate } from '@formkit/auto-animate/vue' import { createHead } from '@unhead/vue/client' import { initializeHybridly } from 'virtual:hybridly/setup' import { createApp } from 'vue' initializeHybridly({ enhanceVue: (vue) => { // [!code focus:5] vue .use(createHead()) .use(autoAnimate) }, }) ``` ## `cleanup` * **Type**: `bool` * **Default**: `true` Defines whether to remove the `data-payload` attribute from the generated element. Note that this is not a security measure, but an aesthetic (and quite useless) one. ## `devtools` * **Type**: `bool` * **Default**: `true` Defines whether to register the Vue DevTools plugin when initializing Hybridly. ## `viewTransitions` * **Type**: `bool` * **Default**: `true` Defines whether [view transitions](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API#the_view_transition_process) are enabled. Currently, they are only supported on Chromium-based browsers and Hybridly provides no fallback. ## `responseErrorModals` * **Type**: `bool` * **Default**: `true` in development, `false` otherwise Defines whether to display an error modal when a hybrid request receives an invalid response. By default, this is `true` only in development. ## `progress` * **Type**: `false` or `ProgressOptions` When set to `false`, disables the built-in progress indicator. Otherwise, configures it. Refer to the [progress indicator](../../guide/progress-indicator.md) documentation for more information. ## `plugins` * **Type**: `Plugin[]` Defines the plugins that should be registered. Refer to the [plugin documentation](../../guide/plugins.md) to learn more about them. ## `defaultFormOptions` * **Type**: `FormOptions` Specifies default options that will be applied to all `useForm` instances. More specific options will take precedence. ## `layout` * **Type**: `Component | Component[] | (() => Component | Component[])` Defines a default layout that is applied to every view that does not already define its own `layout` option. ```ts import { initializeHybridly } from 'virtual:hybridly/setup' initializeHybridly({ layout: UApp }) initializeHybridly({ layout: () => UApp }) initializeHybridly({ layout: [UApp, CustomLayout] }) ``` ## `http` * **Type**: `HttpClient` Defines a custom HTTP client instance that will replace the one Hybridly would internally use otherwise. ```ts import { createXhrHttpClient, initializeHybridly } from 'hybridly' initializeHybridly({ http: createXhrHttpClient(), // [!code focus] }) ``` ## `setup` * **Type**: `(options: SetupArguments) => MaybePromise` Defines a callback that overrides how the Vue application will be created. It accepts an object with the necessary properties to set up Hybridly: * `element`: the DOM element to which Vue should be mounted on * `wrapper`: the wrapper component responsible for Hybridly to work * `render`: the render function that should be passed to `createApp` * `hybridly`: the Vue plugin that sets up Vue DevTools * `payload`: an object representing the initial payload By default, `setup` is optional because the Vue application is created under the hood. It can be enhanced through [`enhanceVue`](#enhancevue). ### Example ```ts import { initializeHybridly } from 'virtual:hybridly/setup' import { createApp } from 'vue' initializeHybridly({ setup: ({ render, element, hybridly }) => createApp({ render }) .use(hybridly) .mount(element), }) ``` ## `id` * **Type**: `string` Defines the `id` of the element the Vue application should be mounted on. When changing this value to something else than `root`, it must be updated in the [directive](../laravel/directives.md#id) as well. ## `serializer` * **Type**: `{ serialize: (data: object) => object; unserialize: (data: object) => object}` Provides custom serialization functions that are used when saving and loading data from the history state. By default, the state is serialized using `JSON.parse(JSON.stringify(data))`, and unserialized as-is. --- --- url: /api/utils/register-hook.md --- # `registerHook` ## Usage `registerHook` takes two mandatory arguments: the [name of the hook](../../guide/hooks.md#available-hooks), and a callback that takes as a parameter the data from this hook. It returns another function that will unregister the hook. When the `once` option is set to `true`, the hook will only be executed once. ```ts registerHook('navigated', ({ payload }) => { console.log(`Navigated to ${payload.url}`) }, { once: true }) ``` --- --- url: /api/utils/route.md --- # `route` ## Usage ```ts function route( name: T, parameters?: RouteParameters, absolute?: boolean, ): string ``` `route` requires at least a route name. It accepts the route's parameters as the second argument, and a boolean that determines if the URL should be absolute, which is the default, as the third. The route name and parameters have TypeScript support through the Vite plugin. To learn how to set it up, read the [routing documentation](../../guide/routing.md#generating-urls). :::info Notes * If the route has mandatory parameters and they are not provided, the `route` function will throw an error. * This function returns a non-reactive string. To make it reactive, wrap it in a computed property or use a watcher. ::: ## Example The example below shows how to generate a URL based on a defined route. ```php // routes/web.php Route::get('/users/{user}', [UsersController::class, 'show']) ->name('users.show') ``` ```vue ``` --- --- url: /api/utils/set-property.md --- # `setProperty` ## Usage `setProperty` accepts the property name as its first parameter and the property value as its second. To update a nested property, you may use a dot-notated path. :::tip Advanced API In most cases, you should use [partial reloads](../../guide/partial-reloads.md) instead. ::: ### Global properties ```ts const name = useProperty('security.user.full_name') console.log(name) // Jon Doe setProperty('security.user.full_name', 'Jane Doe') console.log(name) // Jane Doe ``` ### Local properties Since local properties can't benefit from global typings, you may use a generic to specify its type. ```ts const $props = defineProps<{ users: number }>() console.log($props.users) // 41 setProperty('users', 42) console.log($props.users) // 42 ``` --- --- url: /api/utils/use-back-forward.md --- # `useBackForward` ## Usage ```ts interface UseBackForwardOptions { /** * Calls `reloadOnBackForward` immediately. */ reload: boolean | HybridRequestOptions } function useBackForward(options?: UseBackForwardOptions): { onBackForward: (fn: BackForwardCallback) => void reloadOnBackForward: (options: HybridRequestOptions) => void } ``` Calling `useBackForward` creates a scope in which callbacks are registered. It returns `onBackForward` and `reloadOnBackForward`, which add a callback to the scope when called. ## Examples The following example reloads the page when a back or forward browser navigation is made, in order to refresh potentially-stale data. ```vue ``` The following example calls the defined callback when a back or forward browser navigation is made. ```vue ``` --- --- url: /api/utils/use-dialog.md --- # `useDialog` ## Usage `useDialog` doesn't accept any option. ```ts const { show, close, unmount } = useDialog() ``` It returns a `close` and `unmount` function which control the currently displayed dialog, as well as a `show` property that defines whether the dialog should be shown. ## Options ### `show` * **Type**: `Computed` Defines whether the dialog should be shown. Generally, it is used to control the transition components wrapping the dialog. ### `close` * **Type**: `() => void` Closes the dialog, effectively assigning `false` to `show`. ### `closeLocally` * **Type**: `() => void` Closes the dialog without making a server round-trip, effectively assigning `false` to `show`. ### `unmount` * **Type**: `() => void` Removes the dialog component from the DOM. It should be called after all transitions are finished, generally as a callback to the `after-leave` transition event. --- --- url: /api/utils/use-form.md --- # `useForm` ## Usage ```ts function useForm(options: FormOptions): FormReturn interface FormOptions extends HybridRequestOptions { fields: T key?: string | false timeout?: number resetOnSuccess?: boolean setDefaultOnSuccess?: boolean transform?: (fields: T) => T } ``` `useForm` accepts a object of options, most of which are the same as [router's options](../router/options.md). ## Example The following example is a typical `useForm` usage: ```ts const login = useForm({ method: 'POST', url: route('login'), fields: { email: '', password: '', }, }) ``` ## Form options `useForm` uses most of the options available in the [router's options](../router/options.md). Additionally, the few options specific to this composable are documented below. ### `fields` * **Type**: `object` * **Required**: true Defines the shape of the form data. It is mandatory and provides typings for other form functionality, such as `errors` or `transform`. ### `resetOnSuccess` * **Type**: `boolean` * **Default**: `true` Defines whether the fields should be reset when the submission is successful. ### `setDefaultOnSuccess` * **Type**: `boolean` * **Default**: `false` Defines whether the current fields should be set as the default fields after the submission is successful. If `true`, subsequent `reset` calls will reset the form to use these fields. ### `timeout` * **Type**: `number` * **Default**: `5000` Defines the delay after which the `recentlySuccessful` and `recentlyFailed` variables are reset to `false`. ### `transform` * **Type**: `(fields: T) => any` Defines a transformer function that will be called before submitting data. The data submitted will be the result of this function instead of the fields. ### `key` * **Type**: `string` * **Default**: `false` Defines the key under which the form should be remembered in the history state. This is disabled by default to avoid mixing form states when there are multiple ones in the same page. ## Form return values `useForm` returns an object containing a few variables and utils. ### `fields` * **Type**: `T` A reactive variable containing the fields of the form. You can, for instance, use this variable with `v-model`. ```vue ``` ### `submit` * **Type**: `(options?: HybridRequestOptions) => Promise` A function that submits the form with the given options. These will override the options defined in the form's initialization. ```ts form.submit({ url: '/login' }) ``` ### `hasErrors` * **Type**: `boolean` Returns whether the form has errors. ### `errors` * **Type**: `Record` An object containing the error for each one of the fields. ### `isDirty` * **Type**: `boolean` Returns whether the form has pending modifications relative to its [loaded](#loaded) state. ### `hasDirty` * **Type**: `(...keys: keyof T) => boolean` A function that returns a boolean value that indicates if the given fields are dirty. ### `processing` * **Type**: `boolean` Returns whether the form is being processed — that is, if a navigation triggered by this form is being made. ### `successful` * **Type**: `boolean` Returns whether the form's submission was successful. This value is reset when submitting the form again. ### `failed` * **Type**: `boolean` Returns whether the form's submission has failed. This value is reset when submitting the form again. ### `recentlySuccessful` * **Type**: `boolean` Returns whether the form's was recently successful. This value is reset after the [timeout](#timeout) defined in the form's option, or 5 seconds by default. ### `recentlyFailed` * **Type**: `boolean` Returns whether the form's has recently failed. This value is reset after the [timeout](#timeout) defined in the form's option, or 5 seconds by default. ### `reset` * **Type**: `(...keys: keyof T) => void` A function that resets the given fields, or all fields if none are given. All cleared fields will also have their errors cleared. ### `clear` * **Type**: `(...keys: keyof T) => void` A function that clears the given fields, or all fields if none are given. This is different than `reset`, in the sense that the fields will specifically be set to `undefined`. ### `progress` * **Type**: `{ event: AxiosProgressEvent; percentage: number }` A reactive object containing the current progress percentage. This object may be undefined when there is no pending navigation. ### `clearErrors` * **Type**: `(...keys: keyof T) => void` A function that clears the errors of the given fields, or all fields if none are given. This is automatically called by [`reset`](#reset). ### `clearError` * **Type**: `(error: keyof T) => void` A function that clear the error of the given field. ### `setErrors` * **Type**: `(errors: Record void` A function that sets the given errors to the given fields. This is usually not needed and should only be used to handle edge-cases. ### `initial` * **Type**: `T` Returns a read-only object containing the initial fields defined when initializing the form. ### `loaded` * **Type**: `T` Returns a read-only object containing the fields loaded when instanciating the form. This object may be different than `initial` in case the form was remembered through the history state. ### `abort` * **Type**: `() => void` A function that aborts the submission. This is the same as calling [`router.abort()`](../router/navigation.md#abort). --- --- url: /api/utils/use-history-state.md --- # `useHistoryState` ## Usage `useHistoryState` accepts a key as its first argument and an initial value as its second. The key is an identifier for storing the initial value. The function returns the saved state if it exists, or the the initial value otherwise. ```vue ``` The example above will save a `name` property in the history state, only for the current browing history entry. When navigating back and then forward, the entry will be restored. When refreshing, the entry will have disappeared. --- --- url: /api/utils/use-properties.md --- # `useProperties` ## Usage `useProperties` does not accept any argument, and returns a `reactive` value of all properties, global and local. ```ts const properties = useProperties() ``` ### Typing extra properties Since local properties can not be inferred, the function accepts a generic argument for typing them. ```ts const properties = useProperties<{ foo: string }>() console.log('foo') ``` --- --- url: /api/utils/use-property.md --- # `useProperty` ## Usage `useProperty` accepts a dot-notated path as its first parameter and returns a `ComputedRef` with the value at the given path. ```ts const name = useProperty('security.user.full_name') useHead({ title: () => `${name.value}'s profile`, }) ``` ### Accessing view properties While `useProperty` is primarily made for accessing typed, global properties, you may provide a custom generic type to opt-out of global type-checking if you need to access view-specific properties. ```ts const posts = useProperty('posts') ``` --- --- url: /api/utils/use-query-parameter.md --- # `useQueryParameter` ## Usage You may pass a query parameter name as the first parameter of the function. The return value will be a `Ref` containing the value of that query parameter. This function is specifically useful to avoid passing query parameters from the controller to the page component as properties. ```ts // ?foo=bar const foo = useQueryParameter('foo') console.log(foo.value) // bar ``` ## Options The second parameter of the function accepts an object with the following options: ```ts interface Options { defaultValue?: MaybeRefOrGetter transform?: 'number' | 'bool' | 'string' | 'date' | TransformFn } ``` ### `defaultValue` You may use the `defaultValue` option to specify a value when there is no query parameter: ```ts const count = useQueryParameter('count', { defaultValue: 0 }) console.log(count.value) // 0 ``` The type of the ref will be inferred by the type of the specified `defaultValue`. ### `transform` The `transform` option may be used to transform the query parameter using a custom or predefined function. #### Number Setting `transform` to `number` will convert the query parameter to a number. Note that if the query parameter is not a valid number, this will result in `NaN`. You may specify a `defaultValue` so a missing query parameter does not evaluate to `NaN`. ```ts // ?count=1 const count = useQueryParameter('count', { transform: 'number' }) console.log(count.value) // 1 // no query parameter const count = useQueryParameter('count', { transform: 'number', defaultValue: 0, }) console.log(count.value) // 0 ``` #### Boolean Setting `transform` to `bool` will infer the query parameter as a boolean. ```ts // ?success=1 const success = useQueryParameter('success', { transform: 'bool' }) console.log(success.value) // true ``` #### String Setting `transform` to `string` will ensure the query parameter is a string. ```ts // ?foo=1 const foo = useQueryParameter('foo', { transform: 'string' }) console.log(foo.value) // '1' ``` #### Date Setting `transform` to `date` will convert the query parameter to a date instance. However, no check is performed to ensure the string is a valid date string. ```ts // ?from=2024-01-01 const from = useQueryParameter('from', { transform: 'date' }) console.log(from.value) // Mon Jan 01 2024 01:00:00 GMT+0100 ``` #### Custom function If you have specific logic, you may pass a function as the `transform` option. ```ts // ?count=1 const count = useQueryParameter('count', { transform: (value) => value + 1, defaultValue: 0, }) console.log(count.value) // 2 ``` --- --- url: /api/utils/use-query-parameters.md --- # `useQueryParameters` ## Usage This function is specifically useful to avoid passing query parameters from the controller to the page component as properties. Simply call `useQueryParameters` and access query parameters on the returned object: ```ts // ?foo=bar const parameters = useQueryParameters() console.log(parameters.foo) // bar ``` You may also specify a generic to specify the type of `parameters`: ```ts const parameters = useQueryParameters<{ foo: string }>() ``` --- --- url: /api/utils/use-refinements.md --- # `useRefinements` ## Usage ```ts function useRefinements< Properties extends object, RefinementsKey extends keyof Properties, >( properties: Properties, refinementsKey: RefinementsKey, defaultOptions: HybridRequestOptions = {}, ) ``` `useRefinements` accepts the view's `$props` object as its first parameter and the name of the `Refinement` object's property as the second. As its optional third parameter, it accepts a list of [request options](../router/options.md). ## Example ```ts const $props = defineProps<{ users: Paginator refinements: Refinements }>() const refine = useRefinements($props, 'refinements') ``` ## Returned object The object returned by `useRefinements` contains a few functions and properties that are used to refine the affected query. All of the functions below accept additional [request options](../router/options.md) as their last parameter. ### `sorts` * Type: see the [sorts array](#the-sorts-array). The list of available sorts. They are configured by the `Refine` object in the back-end. ### `filters` * Type: see the [filters array](#the-filters-array). The list of available filters. They are configured by the `Refine` object in the back-end. ### `bindFilter` * Type: `(name: string, options?: BindOptions) => Ref` Binds the given filter to a ref. The second parameter, `options`, accepts an alternative `watch` function and a `debounce` property that defaults to 250 milliseconds. **Example** ```ts const commercial = refine.bindFilter('commercial') const search = refine.bindFilter('search') ``` ### `toggleSort` * Type: `Function` * Parameters: `sortName: string` Toggles the specified sort. `ToggleSortOptions` is the same as `HybridRequestOptions` with an additional `direction` property, which must be undefined, `asc` or `desc`. If specified, the sort will be applied as such. ### `applyFilter` * Type: `Function` * Parameters: `filter: string`, `value: any` Applies the specified value to the specified filter. ### `isFiltering` * Type: `Function` Determines whether the specified filter is active. ### `clearFilter` * Type: `Function` * Parameters: `filter: string` Clears the specified filter. ### `clearFilters` * Type: `Function` Clears all active filters. ### `isSorting` * Type: `Function` Toggles the specified sort. The second parameter accepts a `direction` property that specifies the direction of the sort. Additionnally, the `sortData` property can be used to define additionnal properties that will be added to the request only when the sort is active. ```ts // ?sort=foo&type=bar await refine.toggleSort('foo', { sortData: { type: 'bar', }, }) ``` ### `clearSorts` * Type: `Function` Clears all active sorts. ### `currentSorts` * Type: `Array` The list of currently active sorts. ### `currentFilters` * Type: `Array` The list of currently active filters. ### `getFilter` * Type: `(name: string): FilterRefinement|undefined` Gets a filter object by name. ### `getSort` * Type: `(name: string): SortRefinement|undefined` Gets a sort object by name. ### `reset` * Type: `Function` Resets all filters and sorts. ## The `sorts` array This array contains an entry for each available sort. Each entry extends [`SortRefinement`](#interfaces) and adds the `toggle`, `isSorting` and `clear` methods. These methods are shorthands to [`toggleSort`](#togglesort), [`isSorting`](#issorting) and [`clearSort`](#clearsort) respectively, without the need for the sort name parameter. ## The `filters` array This array contains an entry for each available filter. Each entry extends [`FilterRefinement`](#interfaces) and adds the `apply` and `clear` methods. These methods are shorthands to [`applyFilter`](#applyfilter) and [`clearFilter`](#clearfilter) respectively, without the need for the filter name parameter. ## Interfaces The following are relevant interfaces used by `useRefinements` and its return value. ```ts /** * Base interface for all filter refinements. */ export interface BaseFilterRefinement { /** * Whether this filter is currently active. */ is_active: boolean /** * A string-based icon identifier. */ icon?: string /** * The type of this filter. */ type: string /** * The label of the filter. */ label: string /** * The name of the filter. */ name: string /** * The current value of the filter. */ value: any /** * The current search query of the filter. */ search_query?: string /** * The current options of the filter. */ options?: Record /** * Whether this filter is hidden. */ hidden: boolean /** * The default value of the filter. */ default: any /** * The current operator of the filter. */ operator?: FilterOperator /** * The default operator of the filter. */ default_operator?: FilterOperator /** * The list of supported operators for this filter. */ supported_operators?: FilterOperator[] /** * The metadata attributes of the filter. */ metadata: { /** * A string-based icon identifier. */ icon?: string /** * The label of the filter, suitable for display purposes. */ label?: string /** * A label suitable for previewing this filter in a compact context, like a pill. */ preview_label?: string /** * A string-based identifier suitable for previewing this filter in a compact context, like a pill. */ current_value_label?: string /** * A string-based icon identifier suitable for displaying next to the current value of this filter in a compact context, like a pill. */ current_value_icon?: string /** * A custom property. */ [key: string]: any } } /** * Text filter refinement. */ export interface TextFilterRefinement extends BaseFilterRefinement { type: 'text' operator?: FilterOperator } /** * Select filter refinement. */ export interface SelectFilterRefinement extends BaseFilterRefinement { type: 'select' operator?: FilterOperator metadata: BaseFilterRefinement['metadata'] & { /** * Whether multiple options can be selected. */ is_multiple?: boolean /** * Whether the select filter is searchable. */ is_searchable?: boolean /** * Available options for the select filter. */ options?: Record /** * Label for the selected options. */ selected_options_label?: string /** * Whether the filter allows an empty relationship option. */ allows_empty_relationship_option?: boolean /** * Label for the empty relationship option. */ empty_relationship_option_label?: string /** * Label for the empty search result. */ empty_search_result_label?: string } } /** * Ternary filter refinement. */ export interface TernaryFilterRefinement extends BaseFilterRefinement { type: 'ternary' operator?: FilterOperator metadata: BaseFilterRefinement['metadata'] & { /** * Label for the true state. */ true_label?: string /** * Label for the false state. */ false_label?: string /** * Placeholder label. */ placeholder?: string } supported_operators?: never } /** * Boolean filter refinement. */ export interface BooleanFilterRefinement extends BaseFilterRefinement { type: 'boolean' operator?: FilterOperator metadata: BaseFilterRefinement['metadata'] & { /** * Label for the true state. */ true_label?: string /** * Label for the false state. */ false_label?: string } } /** * Numeric filter refinement. */ export interface NumericFilterRefinement extends BaseFilterRefinement { type: 'numeric' operator?: FilterOperator } /** * Time suggestion for single date filters. */ export interface TimeSuggestion { type: 'time' label: string date: string } /** * Timeframe suggestion for date range filters. */ export interface TimeframeSuggestion { type: 'timeframe' label: string start: string end: string } /** * Date filter refinement. */ export interface DateFilterRefinement extends BaseFilterRefinement { type: 'date' operator?: FilterOperator metadata: BaseFilterRefinement['metadata'] & { /** * Whether this is a timeframe filter (with start and end columns). */ is_timeframe?: boolean /** * The start column for timeframe filters. */ start_column?: string /** * The end column for timeframe filters. */ end_column?: string /** * Suggested dates or timeframes for this filter. */ suggestions?: Array /** * A description for this filter, suitable for display purposes. */ description?: string } } /** * Trashed filter refinement. */ export interface TrashedFilterRefinement extends BaseFilterRefinement { type: 'trashed' supported_operators?: never } /** * Callback filter refinement. */ export interface CallbackFilterRefinement extends BaseFilterRefinement { type: 'callback' | (string & {}) supported_operators?: never } /** * Represents a filter. */ export type FilterRefinement = | TextFilterRefinement | SelectFilterRefinement | TernaryFilterRefinement | BooleanFilterRefinement | NumericFilterRefinement | DateFilterRefinement | TrashedFilterRefinement | CallbackFilterRefinement export interface SortRefinement { /** * Whether this sort is currently active. */ is_active: boolean /** * The current direction of the sort. */ direction?: SortDirection /** * The default direction of the sort. */ default?: SortDirection /** * The label of the sort. */ label: string /** * The metadata attributes of the sort. */ metadata: Record /** * The name of the sort. */ name: string /** * The value corresponding to the descending sort. */ desc: string /** * The value corresponding to the ascending sort. */ asc: string /** * The value that will be applied on toggle. */ next: string /** * Whether this sort is hidden. */ hidden: boolean } export interface Refinements { /** * The list of available filters. */ filters: Array /** * The list of available sorts. */ sorts: Array /** * The URL scope for these refinements. */ scope?: string /** * The scope keys for these refinements. */ keys: { /** * The scope key for sorting. */ sorts: string /** * The scope key for filtering. */ filters: string } } ``` --- --- url: /api/utils/use-route.md --- # `useRoute` ## Usage `useRoute` doesn't accept any option. ```ts const { isNavigating, current, matches } = useRoute() ``` ## Utilities ### `isNavigating` * **Type**: `Ref` Ref that determines whether a navigation is occuring. ### `current` * **Type**: `Ref` Ref that contains the current route name. It may be `undefined` if the current route has no defined name. ### `matches` * **Type**: `(name: MaybeRef, parameters?: RouteParameters) => boolean` Determines whether the given `name` matches the current route name. Providing parameters would also ensure the current route parameters match. ```ts matches('tenant.*') matches('profile', { user: currentUserId }) ``` --- --- url: /api/utils/use-table.md --- # `useTable` ## Usage ```ts function useTable< Properties extends Record, Paginator extends 'simple' | 'cursor' | 'length-aware', >( properties: Properties, tableKey: keyof Properties, defaultOptions: HybridRequestOptions = {}, ) ``` `useTable` accepts the view's `$props` object as its first parameter and the name of the `Table` object's property as the second. As its optional third parameter, it accepts a list of [request options](../router/options.md). ## Example ```ts const $props = defineProps<{ users: Table }>() const users = useTable($props, 'users') ``` ## Records The `records` property returns a computed list of `Record` objects with the following properties: ```vue-html ``` ### `value` * Type: `Function` * Parameters: `column: Column` Gets the value of the record for the specified column. ### `extra` * Type: `Function` * Parameters: `column: Column` and `path: string` Gets the extra data of the record for the specified column. The second parameter is the dot-notation-enabled path to the extra property you want to access. ### `key` * Type: `string | int` The key of the record. Generally, it is the value of the `id` column. It is used for executing actions, but may be missing if [actions are disabled](../../guide/tables.md#disabling-actions-globally). ### `execute` * Type: `Function` * Parameters: `action: Action` Executes the given inline action for the record. ### `actions` * Type: `Action[]` The list of inline actions. Each `Action` has an additional scoped `execute` function. ### `select` * Type: `Function` Selects this record. ### `deselect` * Type: `Function` Deselects this record. ### `toggle` * Type: `Function` Toggles selection for this record. ### `selected` * Type: `bool` Checks whether this record is selected. ### `record` * Type: `T` The actual record. Its type is determined by the first generic of `useTable`. ## Actions The following functions and properties are used to deal with actions. All `Action` objects use the following interface: ```ts export interface Action { /** The name of this action. */ name: string /** The label of this action. */ label: string /** The type of this action. */ type: string /** Custom metadata for this action. */ metadata: any /** A user-defined URL to which to post the action. */ url?: string } ``` ### `bulkActions` * Type: `Action[]` Returns a list of available bulk actions. Hidden actions are not part of the list. The returned actions contain an extra `execute` function that can be used to call the action for all [selected records](#bulk-selection): ```vue-html
``` ### `inlineActions` * Type: `Action[]` Returns a list of available inline actions. Hidden actions are not part of the list. The returned actions contain an extra `execute` function that can be used to call the action for the specified record: ```vue-html
``` Note that the recommended way to use inline actions is through the `table.records.actions` property, which doesn't need a record to be specified. ### `executeInlineAction` * Type: `Function` * Parameters: `action: Action`, `record: Record` This function executes the given inline action for the given record. ### `executeBulkAction` * Type: `Function` * Parameters: `action: Action`, `options?: BulkActionOptions` This function executes the given bulk action for all [selected records](#bulk-selection). ## Columns The `columns` property returns a computed list of `Column` objects with the following properties: ### `name` * Type: `string` The name of the column. ### `label` * Type: `string` The label of the column. ### `metadata` * Type: `Record` Custom [metadata](../../guide/tables.md#adding-metadata) for the column. ### `isSortable` * Type: `bool` Checks whether the column has a corresponding sort. ### `toggleSort` * Type: `Function` * Parameters: `options?: ToggleSortOptions` Toggles sorting for this column, provided it has a corresponding sort. ### `isSorting` * Type: `Function` * Parameters: `direction?: SortDirection` Checks whether the column is being sorted. ### `isFilterable` * Type: `bool` Checks whether the column has a corresponding filter. ### `applyFilter` * Type: `Function` * Parameters: `value: any`, `options?: AvailableHybridRequestOptions` Applies the filter with the same name as the column, if it exists. ### `clearFilter` * Type: `Function` * Parameters: `options?: AvailableHybridRequestOptions` Clears the filter with the same name as the column, if it exists. ## Bulk-selection The following functions and properties are used to deal with selecting records. ### `selection` The `selection` objects represents the records that are selected. It uses the following interface: ```ts export interface BulkSelection { /** Whether all records are selected. */ all: boolean /** Included records. */ only: Set /** Excluded records. */ except: Set } ``` When selecting *all* records, instead of adding all possible record identifiers in a set, the `all` boolean will be set to `true`. In this situation, records may be de-selected by being added to `except`. This is all done *automaticaly* by the helpers bellow. ### `selectAll` * Type: `Function` Selects all records. ### `deselectAll` * Type: `Function` De-selects all records. ### `selectPage` * Type: `Function` Selects all records on the current page. ### `deselectPage` * Type: `Function` De-selects all records on the current page. ### `isPageSelected` * Type: `Computed` Checks if all records on the current page are selected. ### `isSelected` * Type: `Function` * Parameters: `record: T` Checks if the given record is selected. ### `toggle` * Type: `Function` * Parameters: `record: T` Toggles selection for the given record. ### `select` * Type: `Function` * Parameters: `record: T` Selects the given record. ### `deselect` * Type: `Function` * Parameters: `record: T` Deselectsthe given record. ### `allSelected` * Type: `Computed` Checks if all records are selected. ## Pagination A `paginator` object is returned. Its shape is typed and depends on the second generic of [`useTable`](#usage), which should be one of `simple`, `cursor` or `length-aware`. You can learn more about the supported paginators in the [tables documentation](../../guide/tables.md#supported-paginators). ## Refinement The preferred way of dealing with refinement using `useTable` is to use the scoped refinement helpers available in its properties. However, for convenience, all the properties and functions returned by [`useRefinements`](./use-refinements.md#returned-object) are also available. --- --- url: /guide/architecture.md --- # Architecture configuration ## Overview By default, Hybridly expects view and layout files to be stored somewhere in the `resources` directory or its subdirectories. However, storing view files far apart from their related code can be inconvenient. For instance, if you have a `Billing` domain, you might want to store its views and layouts in `src/Billing` instead of `resources`. For this reason, Hybridly provides the ability to configure where it looks for view and layout files. ## Default architecture By default, Hybridly uses the following file structure: ```text resources/ ├── main.ts ├── root.blade.php ├── index.view.vue ├── security/ │ ├── register.view.vue │ └── login.view.vue └── default.layout.vue ``` As you can see, the front-end lives in `resources`, including the root Blade layout and the `main.ts` entrypoint. There is no `resources/views` or `resources/layouts` subdirectory, as this would affect the identifiers for these components. ## Modular architecture If the default architecture is not suitable, you may update the `architecture` configuration in `config/hybridly.php`: ```php 'architecture' => [ // ... 'root_directory' => 'resources', // [!code --] 'component_loader' => ResourcesComponentLoader::class, // [!code --] 'root_directory' => 'app', // [!code ++] 'component_loader' => ModulesComponentLoader::class, // [!code ++] ], ``` * The `component_loader` option accepts a class name that implements the `ComponentLoader` interface. This class is responsible for finding view and layout files in the file system and registering them. * The `root_directory` option defines the directory in which Hybridly expects to find the `main.ts` and `root.blade.php` files. ### Example Using the configuration above, you would typically organize your code by modules or vertical slices in your main namespace: ```text app/ ├── main.ts ├── root.blade.php ├── default.layout.vue └── Authentication/ ├── User.php ├── RegisterUserController.php └── register.view.vue ``` In this example, the `register.view.vue` file would be identifier by `authentication.register`. ## Configuration reference The following options are available for the `architecture` configuration in `config/hybridly.php`: ### `root_directory` Defines the directory in which Hybridly expects to find the `main.ts` and `root.blade.php` files. ### `component_loader` Defines the class responsible for finding view and layout files in the file system and registering them. By default, it is `ResourcesComponentLoader`, which looks for files in the `resources` directory. Another available option is `ModulesComponentLoader`, which looks for files in the `src` directory. ### `eager_load_views` Defines whether to enable code-splitting. When enabled, all view and layout files will be loaded on the first request. This is a good default for small to medium applications. ### `generate_absolute_urls` Defines whether to generate absolute URLs when using the [`route`](../api/utils/route.md) util. ### `entrypoint` Defines the name of the entrypoint file. By default, it is `main.ts`. ### `root_view` Defines the name of the root view. By default, it is `root`. --- --- url: /guide/asset-versioning.md --- # Asset versioning ## Overview Hybridly has a built-in mechanism for forcing a full page reload when assets are updated. Without an asset reloading mechanism, a user could be on an outdated page that could behave unexpectedly and potentially crash after a deploy. This mechanism works by sending a version hash back-and-forth between the front-end and back-end adapters. When the hash mismatches, Hybridly will respond with an external redirection, effectively causing a full page reload. ## Controlling the version Hybridly has sensible defaults for controlling the version. It looks for a manifest generated by Vite or by the `app.asset_url` option in case the application is hosted on Vapor. This logic can be extended or overriden by extending the `version` method of the middleware. Additionally, calling `hybridly()->resolveVersionUsing(fn () => $version)` will override the version for the next request—this can be used to forcefully invalidate the next response. ## Cache busting Cache-busting is a technique for invalidating cache in browsers. Without it, browsers could use a cached asset for as long as its time-to-live is valid. Hybridly does not enforce cache-busting by itself—but Vite does by default. If you manually overwrote Rollup's `chunkFileNames`, make sure you keep the hash or use a differnet cache-busting strategy. --- --- url: /guide/authentication.md --- # Authentication ## Overview One of the benefits of Hybridly is that it acts like a classic monolithic application. There is no need for a token-based authentication system like the one provided by [Laravel Sanctum](https://laravel.com/docs/9.x/sanctum), or an advanced authentication system like OAuth. Hybridly works best with session-based authentication systems, such as what Laravel provides by default. ## Sharing user data In most application requiring authentication, you need to share some information regarding the currently logged-in user across the application. The [global properties documentation](./global-properties.md) explains how to achieve this by using a middleware. :::code-group ```php [app/Users/ShareUserData.php] use Hybridly\Hybridly; use Illuminate\Auth\AuthManager; final readonly class ShareUserData { public function __construct( private Hybridly $hybridly, private AuthManager $auth, ) {} public function __invoke(Request $request, Closure $next): Response { if (! $this->auth->check()) { return $next($request); } $this->hybridly->persist('user'); $this->hybridly->share(new UserData( name: $user->name, email: $user->email, )); return $next($request); } } ``` ::: In this example, the `ShareUserData` middleware shares a `user` property with the client containing the name and email of the currently logged-in user. The shape of the `user` object is defined by the `UserData` data object, which types can be [automatically generated](./typescript.md). On the front-end, you may use [`useProperty`](../api/utils/use-property.md) to read the `user` property and get the user data. ```vue [resources/default.layout.vue] ``` --- --- url: /guide/authorization.md --- # Authorization ## Overview Authorization is what ensures an entity has the ability to perform a given task. Laravel provides gates and policies — they are simple but powerful ways to answer this problem. Authorization needs to be performed on the server. This is usually done through `User#can` or `Gate::authorize`. Unfortunately, this is not accessible when working in single-file components. ## Authorizing on the front-end The recommend approach is to share authorization information as part of the shared data. This way, you can easily check for permissions on the front-end without having to make additional requests. :::code-group ```php [App/Users/ShowUsersController.php] final class ShowUsersController { public function __invoke(): HybridResponse { $users = User::query() ->where('active', true) ->get(); return view('users.index', [ 'can' => [ 'create_user' => Auth::user()->can('create', User::class), ], 'users' => UserData::collect($users, into: 'array'), ]); } } ``` ```php [App/Users/UserData.php] final class UserData extends Data { public function __construct( public readonly string $name, public readonly string $email, public readonly bool $can_update, ) {} public static function fromModel(User $user): self { return new self( name: $user->name, email: $user->email, can_update: Auth::user()->can('update', $user), ); } } ``` ::: ## Sharing authorizations globally If you need authorization information to be available globally, you can share it through a dedicated middleware. ```php use Hybridly\Hybridly; final readonly class ShareAuthorizations { public function __construct( private Hybridly $hybridly, ) {} public function __invoke(Request $request, Closure $next): Response { $this->hybridly->persist('authorizations'); $this->hybridly->share('authorizations', new AuthorizationData( can_create_user: $request->user()->can('create', User::class), can_create_post: $request->user()->can('create', Post::class), )); return $next($request); } } ``` Learn more on the [global properties](../guide/global-properties.md) documentation. --- --- url: /guide/case-conversion.md --- # Case conversion ## Overview PHP and TypeScript have their own conventions when it comes to coding style, specifically regarding objects and arrays. While it's common to have `snake_case` array keys in PHP, TypeScript objects are generally written using `camelCase`. Vue's style guide [also recommends using `camelCase` when defining properties](https://eslint.vuejs.org/rules/prop-name-casing.html) in single-file components. Hybridly provides a way to keep using `snake_case` when defining hybrid properties, while still receiving them in `camelCase` in single-file components. ## Enabling case conversion By default, in order to avoid any confusion, this feature is disabled. It may be configured in `config/hybridly.php`. ```php return [ 'force_case' => [ 'input' => 'snake', 'output' => 'camel', ], ]; ``` The `input` option converts properties that are sent to Laravel, specifically partial properties. In the example above, partial properties can be referred to using any case, as long as they are using `snake_case` in PHP code. The `output` option converts properties that are sent to single-file components. In the example above, properties can be sent to single-file components using any case, and single-file components will receive them as `camelCase`. :::info Top-level properties only Note that this feature only applies to top-level properties. While it could be implemented for nested properties as well, this could create a lot of confusion and would interfere with generated TypeScript definitions. ::: --- --- url: /guide/debugging.md --- # Debugging ## Using Vue Devtools The Vue plugin provided by Hybridly integrates with Vue DevTools. It makes debugging hybrid views convenient. Make sure the [Vue DevTools](https://devtools.vuejs.org/) extension is installed in your browser, and open the Vue tab in the developer tools. Selecting any component will show a `hybridly` section with the active component name, properties, asset version, url and the routes registered by the router. The Hybridly wrapper component shown below the root component displays the context, an object that contain the whole state of Hybridly's core. ## Using the console Hybridly implements [debug](https://github.com/debug-js/debug), a lightweight debug utility that allows logging data. To enable logging, you may add the `debug:*` key/value pair to your application's local storage. You may also be more specific and chose to only log certain values instead of using `*`. ## In tests During tests, you may use the `hdd` macro on `TestResponse` instances. ```php test('guests can see the login page', function () { get('/login') ->hdd() ->assertOk() ->assertHybrid(); }); ``` When using `hdd` on non-hybrid responses, the response's body will be shown instead. --- --- url: /guide/dialogs.md --- # Dialogs ## Overview Dialogs are components with their own URL and properties, which are rendered as siblings to view components. When navigating from a view to a dialog, its component will be mounted as a sibling to the current view component—but when navigated to directly via their URL, the base view component and properties will be loaded first, so the dialog can be rendered on top of it. ## Creating a dialog Dialogs may be anything you want — most commonly, they will be modals or slideovers. The following component is an example of a dialog component which is shown as a modal. :::code-group ```vue [app/Users/edit.view.vue] ``` ```vue [app/Components/hybrid-dialog.vue] ``` ::: In this example, the modal component is implemented as a Nuxt UI [`Modal`](https://ui.nuxt.com/docs/components/modal). Notice the call to [`unmount`](../api/utils/use-dialog.md#unmount), which is needed to remove the dialog from the DOM after it's closed and its animations have finished. ## Rendering a dialog Dialogs are rendered the same as normal view components, but need a base view which can be defined by calling the [`base`](../api/laravel/hybridly.md#base) method on the view factory. The `base` method takes the name of a route and its parameters are arguments. When the dialog is accessed directly via its URL, this route will be used to determine which background view should be used. ```php use App\Users\UserData; use App\Users\User; use function Hybridly\view; final class UserController { public function edit(User $user): HybridResponse { Gate::authorize('update', $user); return view('users.edit', [ 'user' => UserData::from($user), ])->configureDialog(baseUrl: route('user.show', $user)); } } ``` ## Forcing the base page to be displayed If you want the base page of a dialog to always be displayed, even when the dialog is opened from a different page, you may set the `alwaysRedirectToBase` parameter of the `base` method to `true`: ```php return view('users.edit', ['user' => UserData::from($user)]) ->configureDialog( baseUrl: route('user.show', $user), alwaysRedirectToBase: true, ); ``` This means that every time that dialog is opened, its background page will no longer be the current page, but the specified base page. ## Keeping the current state of the base page For performance reasons, you may prefer that the properties of the base page are not updated. You may achieve this by setting the `preserveBaseOnClose` parameter of the `base` method to `true`: ```php return view('users.edit', ['user' => UserData::from($user)]) ->configureDialog( baseUrl: route('user.show', $user), preserveBaseOnClose: true, ); ``` This means that every time that dialog is opened, its background page will no longer be updated. ## Closing a dialog Navigating away from a dialog will automatically close it. In addition to the `close` function returned by [`useDialog`](../api/utils/use-dialog.md), it's possible to call `router.dialog.close()`. This function takes the same [options](../api/router/options.md) as any navigation. ```vue ``` --- --- url: /api/laravel/directives.md --- # Directives ## `@hybridly` This directive creates the markup required to initialize Vue with Hybridly and its initial data. Its arguments are optional, and, if provided, must use the named argument syntax. ### Usage ```blade @vite('resources/application/main.ts') @hybridly(class: 'flex flex-col') // [!code focus] ``` ### `id` Sets the `id` of the generated HTML element. By default, `root` is used. Note that changing the `id` there requires changing it in [`initializeHybridly`](../utils/initialize-hybridly.md) as well. ```blade @hybridly(id: 'app') ``` ### `class` Sets the `class` of the generated HTML element. ```blade @hybridly(class: 'flex flex-col') ``` ### `element` Defines which element will get generated. By default, a `div` is used. ```blade @hybridly(element: 'main') ``` --- --- url: /guide/exception-handling.md --- # Exception handling ## Overview During development, when a non-hybrid response is returned from a hybrid request, it will be displayed in a modal. The navigation will be cancelled, and the modal will be dismissible. In other words, Laravel's exception handling keeps working as expected, and the debugging experience is the same as usual. ## Production By default, in production, exceptions have the same behavior. Presenting an error modal with no information is not particularly a good user experience, so you will probably want to handle them and return a proper response page. Hybridly makes this fairly simple by providing a `renderExceptionsUsing` method on the `Hybridly` instance. It must be given a callback that is responsible for returning a response, which is usually a hybrid view. Typically, this is done in a service provider: :::code-group ```php [app/Providers/AppServiceProvider.php] use Hybridly\Hybridly; public function boot(Hybridly $hybridly): void { $hybridly->renderExceptionsUsing(fn (Response $response) => view('error', [ 'status' => $response->getStatusCode() ])); } ``` ::: ## Session expiration By default, when a session expires, Laravel throws a `TokenMismatchException` that renders as an `HTTP 419` code. You can catch these exceptions by using the `handleSessionExpirationUsing` method on the `Hybridly` instance. ```php [app/Providers/AppServiceProvider.php] use Hybridly\Hybridly; public function boot(Hybridly $hybridly): void { $hybridly->handleSessionExpirationUsing(fn () => back()->with([ 'error' => 'Your session has expired. Please refresh the page.', ])); } ``` ## Previewing exceptions locally When working on your exception page, you might not want to receive the default error modal that Hybridly uses during development. You can instruct Hybridly to render your exception defined using `renderExceptionsUsing` by using `renderExceptionsInDevelopment`: :::code-group ```php [app/Providers/AppServiceProvider.php] use Hybridly\Hybridly; public function boot(Hybridly $hybridly): void { $hybridly->renderExceptionsInDevelopment(); // [!code hl] $hybridly->renderExceptionsUsing(fn (Response $response) => view('error', [ 'status' => $response->getStatusCode() ])); } ``` ::: --- --- url: /guide/file-uploads.md --- # File uploads ## Overview When submitting data that include files, Hybridly will automatically convert the request data into a [`FormData` object](https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects). This is necessary, as file uploads are only possible when a form encoding is set to `multipart/form-data`. It's also possible to send a `FormData` object directly, or force the conversion using `useFormData`. ```ts const data = new FormData() // ... router.post(url, { data, }) ``` ## Tracking upload progress When using the form util, a `progress` object is exposed. It contains the current progress `percentage` and the associated `HttpUploadProgressEvent`. ```ts const form = useForm() /* ... */ // form.progress?.percentage // form.progress?.event ``` The example below shows how a file can be uploaded and its progress be displayed. ```vue ``` ## Limitations It is not natively supported by PHP to upload files using `multipart/form-data` and the `PUT`, `PATCH` or `DELETE` methods. Fortunately, Laravel supports [method spoofing](https://laravel.com/docs/9.x/routing#form-method-spoofing), which means one can add a `_method` field with `PUT`, `PATCH` or `DELETE` and send the request using `POST`. Hybridly automatically does this when detecting files and the `PUT`, `PATCH` or `DELETE` method, unless [`spoof`](../api/router/options.md#spoof) is set to `false`. ```ts const avatar = new File() // ... router.post(url, { data: { _method: 'PUT', avatar, }, }) ``` --- --- url: /guide/forms.md --- # Forms ## Overview Hybridly provides a [``](../api/components/form.md) component for declaring forms in templates, and a [`useForm`](../api/utils/use-form.md) composable for fully controlled form state. `` is the simplest option and should be enough for a lot of use cases. It works directly with native fields using `name` attributes, and offers utils in its default slot for working with validation errors. Use `useForm` when you need full control over fields, custom interactions, or `v-model`-based inputs. ## Using `` The `` component reads values from native controls and submits hybrid requests for you. The `action` attribute takes the URL to submit to, and the optional `method` attribute specifies the HTTP method. ```vue ``` ### Error bags When multiple forms coexist, use the `error-bag` attribute to scope validation errors. ```vue ``` ### Disable while processing Use the `disable-while-processing` attribute to apply the native `inert` attribute while a request is being processed. This prevents any interaction with the form until the request is finished. ```vue ``` ### Resetting the form Use the `reset-on-success` attribute to reset fields after a successful submission. This can be useful for forms that create new resources, such as a comment form. ```vue ``` Optionally, you can specify specific fields to reset by passing an array of field names instead: ```vue ``` Similarly, you may use the `reset-on-error` attribute to reset fields after validation errors. ### Setting new default values Use the `set-default-on-success` attribute to set new default values for fields after a successful submission. ```vue ``` ### Default slot `` exposes the underlying `useForm` state in its default slot, plus helpers such as `getError`, `reset`, and `resetFields`. ```vue ``` ## Using `useForm` When more control is needed over form state, you may use the `useForm` composable instead. Typically, the native form submission would be prevented using `@submit.prevent`, and the `submit()` helper would be called to trigger the request instead. ```vue ``` ### Transforming data before submission You may pass a `transform` function to `useForm` to adapt outgoing data before it is sent. This can be useful for adjusting field values, such as converting a boolean to a string for checkboxes, or date objects to a format suitable for the backend. ```ts const form = useForm({ url: route('login'), method: 'POST', fields: { email: '', password: '', remember: true, }, transform: (fields) => ({ ...fields, remember: fields.remember ? 'on' : '', }), }) ``` ### Transforming URLs A convenient way of transforming URLs is provided through the [request options](../api/router/options.md#transformurl). For instance, clearing the query parameters is as simple as passing an empty string to the search property: ```ts const form = useForm({ url: '/filter?sort=asc', // ... transformUrl: { search: '', }, }) ``` ### Request lifecycle Sometimes, you may need to execute custom logic during the request's lifecycle, such as showing a notification when a request starts or finishes. The `useForm` util accepts [request options](../api/router/options.md#transformurl), which have a `hooks` property that can be used to catch these events. ```ts const form = useForm({ // ... hooks: { start: () => console.log('Request started'), 'validation-error': () => console.log('Validation failed'), success: () => console.log('Request succeeded'), after: () => console.log('Request finished'), }, }) ``` --- --- url: /api/laravel/functions.md --- # Functions Hybridly exposes a few global and namespaced utility functions. The functions live in the `\Hybridly` namespace and need to be imported before being used. ## `view` Renders a view with the given component and optional properties. The properties can be an array, an `Arrayable` or a [data object](../../guide/typescript#data-objects). > See also: [responses](../../guide/responses.md) ```php use function Hybridly\view; return view('user.show', $user); ``` ## `properties` Returns updated properties for an existing view. > See also: [properties-only responses](../../guide/responses.md#updating-properties) ```php use function Hybridly\properties; return properties([ 'user' => $user, ]); ``` ## `dialog` Returns a dialog with the given properties and base view. > See also: [dialogs](../../guide/dialogs.md) ```php use function Hybridly\dialog; return dialog( component: 'users.dit', properties: ['user' => $user], base: route('users.show', $user), ); ``` ## `on_demand` Creates a [partial-only](../../guide/partial-reloads.md#partial-only-properties) property. > See also: [partial-only properties](../../guide/partial-reloads.md#partial-only-properties) ```php use function Hybridly\view; use function Hybridly\on_demand; return view('user.show', [ 'user' => $user, 'posts' => on_demand(fn () => Post::forUser($user)->paginate()), ]); ``` ## `merge` Creates a mergeable property. When this property is included in a subsequent response, it is merged with the current frontend value instead of replacing it. ```php use function Hybridly\merge; use function Hybridly\view; return view('users.index', [ 'users' => merge(fn () => UserData::collection(User::latest()->limit(20)->get())), ]); ``` The merge semantics may be configured: * `uniqueBy` specifies a unique key to deduplicate items. * `prepend` specifies whether new items should be prepended instead of appended. * `paths` specifies the paths to merge instead of the root value. ## `deferred` Creates a partial property that will automatically be loaded in a subsequent partial reload when the page loads. > See also: [deferred properties](../../guide/partial-reloads.md#deferred-properties) ```php use function Hybridly\view; use function Hybridly\deferred; return view('user.show', [ 'user' => $user, 'posts' => deferred(fn () => Post::forUser($user)->paginate()), ]); ``` You may optionally provide a group name to defer properties together: ```php deferred(fn () => MetricsData::from($metrics), group: 'metrics') ``` ## `to_external_url` Redirects to a non-hybrid view or an external domain. > See also: [external redirects](../../guide/responses.md#external-redirects). ```php use function Hybridly\to_external_url; return to_external_url('https://google.com'); ``` ## `is_hybrid` Determines whether the current request is hybrid. Optionally, a `Illuminate\Http\Request` instance can be given instead of using the current request. ```php use function Hybridly\is_hybrid; if (is_hybrid()) { // ... } ``` ## `is_partial` Determines whether the current request is a [partial reload](../../guide/partial-reloads.md). Optionally, a `Illuminate\Http\Request` instance can be given instead of using the current request. ```php use function Hybridly\is_partial; if (is_partial()) { // ... } ``` ## `partial_headers` Generates headers for testing partial requests. The first parameter is the view component name, and the second and third parameters are an array of `only` and `except` properties, respectively. > See also: [partial reloads](../../guide/partial-reloads.md) ```php use function Hybridly\Testing\partial_headers; get('/', partial_headers('users.show', only: ['posts'])) ->assertMissingHybridProperty('user') ->assertHybridProperty('posts'); ``` ## `hybridly` This functions returns the [`Hybridly\Hybridly`](./hybridly.md) singleton instance. When possible, you should prefer dependency injection instead. --- --- url: /guide/global-properties.md --- # Global properties ## Overview In most applications, some data needs to be available globally. This is generally the case, for instance, of the logged-in user, for navigation data or for flash notifications. You may achieve this by sharing properties globally, which may be accessed in the front-end using [`useProperty`](../api/utils/use-property.md) or [`useProperties`](../api/utils/use-properties.md). ## From a middleware The idiomatic way of defining global properties is to create a dedicated middleware and to share properties from there. ```php final readonly class ShareNavigation { public function __construct( private Hybridly $hybridly, private NavigationBuilder $navigation, ) {} public function __invoke(Request $request, Closure $next): Response { $this->hybridly->persist(['navigation']); $this->hybridly->share('navigation', $this->navigation->getSidebarItems()); return $next($request); } } ``` It is generally a good idea to create a TypeScript declaration file to inform TypeScript about the properties that are shared globally. For instance, the declaration file for the example above could look like that: ```ts import 'hybridly' declare module 'hybridly' { export interface GlobalHybridlyProperties { navigation: App.Navigation.SharedNavigationItem[] } } export {} ``` This makes [`useProperty`](../api/utils/use-property.md) type-safe and provides your editor with autocompletion. ## From anywhere In the eventual case where sharing data outside the middleware is needed, it is possible through the `hybridly` function. ```php hybridly()->share([ 'foo' => 'bar' ]); ``` While this is a useful escape hatch, it is not recommended. This way of sharing data being dynamic by nature, it is not possible to completely type it. If possible, consider using the middleware instead. ## Accessing global properties The [`useProperty`](../api/utils/use-property.md) function provides typed dot-notation support for accessing global properties. For instance, using the `user` property in the `security` array as shown in the earlier example would look like that: ```ts const user = useProperty('security.user') ``` [`useProperty`](../api/utils/use-property.md) returns a `ref` that will be updated if the data is changed. --- --- url: /guide/hooks.md --- # Hooks ## Overview Hybridly's requests have their own lifecycle. It's sometimes necessary to hook into some of its events to perform custom logic. For instance, the progress bar is implemented using these hooks. There are a two main ways to catch Hybridly's events: globally, through [plugins](./plugins.md), or locally, through [visit options](../api/router/navigation.md). ## Plugins Plugins can be registered through [`initializeHybridly`](../api/utils/initialize-hybridly.md)'s `plugins` option. They have access to all [request lifecycle events](#request-lifecycle-events), plus a few additionnal [plugin-specific hooks](./plugins.md#plugin-specific-hooks). They are useful when you need to execute custom logic for each request. You can learn more about plugins in [their documentation](./plugins.md). ## Registering hooks manually Though it's highly recommended to use plugins, it's also possible to register hooks with the `registerHook` function. This may be useful for requirements for which plugins could be overkill. ```ts ``` ## Visit options When [navigating](./navigation.md) or using the [form util](./forms.md), it's possible to pass a `hook` object that accepts a callback for each lifecycle event. These callbacks will be executed just once for the current request. You can learn more about visit options in [their documentation](../api/router/navigation.md). ## Request lifecycle events The following lifecycle events can be hooked into when making a request, via the [`hooks`](../api/router/options.md#hooks) navigation option. They may be awaited if necessary. ```ts export interface RequestHooks { /* [!code focus:54] */ /** * Called before a navigation request is going to happen. */ before: (request: PendingHybridRequest, context: InternalRouterContext) => MaybePromise /** * Called before the request of a navigation is going to happen. */ start: (request: PendingHybridRequest, context: InternalRouterContext) => MaybePromise /** * Called when progress on the request is being made. */ progress: (progress: HttpUploadProgressEvent, request: PendingHybridRequest, context: InternalRouterContext) => MaybePromise /** * Called when data is received after a request for a navigation. */ data: (request: PendingHybridRequest, response: HttpResponse, context: InternalRouterContext) => MaybePromise /** * Called when a request is successful and there is no error. */ success: (payload: HybridPayload, request: PendingHybridRequest, response: HttpResponse, context: InternalRouterContext) => MaybePromise /** * Called when a request is successful but there were validation errors. */ 'validation-error': (errors: Errors, request: PendingHybridRequest, context: InternalRouterContext) => MaybePromise /** * Called when a request has been aborted. */ abort: (request: PendingHybridRequest, context: InternalRouterContext) => MaybePromise /** * Called when a response to a request is not a valid hybrid response. */ invalid: (request: PendingHybridRequest, response: HttpResponse, context: InternalRouterContext) => MaybePromise /** * Called when an unknown exception was triggered. */ exception: (error: Error, request: PendingHybridRequest, context: InternalRouterContext) => MaybePromise /** * Called whenever the request failed, for any reason, in addition to other hooks. */ fail: (error: Error, request: PendingHybridRequest, context: InternalRouterContext) => MaybePromise /** * Called after a response has been received, even if it didn't succeed in a navigation. */ after: (request: PendingHybridRequest, context: InternalRouterContext) => MaybePromise } ``` --- --- url: /api/laravel/hybridly.md --- # Hybridly ## `view` Returns a `Hybridly\View\Factory` for the given component and optional properties. * See [responses](../../guide/responses.md) for more details. * For dialogs, use the [`dialog` namespaced function](./functions.md#dialog). ### Usage ```php return $this->hybridly->view('users.show', [ 'user' => UserData::from($user), ]); ``` ## `properties` Returns updated properties for the current view. * See [responses](../../guide/responses.md#updating-properties) for more details. ### Usage ```php return $this->hybridly->properties([ 'user' => UserData::from($user), ]); ``` ## `createExternalRedirect` Generates a response that redirects to an external website or non-hybrid endpoint. * See also: [`to_external_url`](./functions.md#to-external-url) * See [external redirects](../../guide/responses.md#external-redirects) for details. ### Usage ```php return $this->hybridly->createExternalRedirect('https://google.com'); ``` ## `onDemand` Creates a property that is only evaluated when explicitly requested through a partial reload. * See also: [`on_demand`](./functions.md#on_demand) * See [partial-only properties](../../guide/partial-reloads.md#partial-only-properties) for more details. ### Usage ```php return $this->hybridly->view('users.show', [ 'user' => UserData::from($user), 'posts' => $this->hybridly->onDemand(fn () => PostData::collection($user->posts)), ]); ``` ## `deferred` Creates a property that is not included in the initial load, but is automatically loaded in a subsequent partial reload. * See also: [`deferred`](./functions.md#deferred) * See [deferred properties](../../guide/partial-reloads.md#deferred-properties) for more details. ### Usage ```php return $this->hybridly->view('users.show', [ 'user' => UserData::from($user), 'stats' => $this->hybridly->deferred( callback: fn () => UserStatsData::from($user), group: 'sidebar', ), ]); ``` ## `isHybrid` * See also: [`is_hybrid`](./functions.md#is-hybrid) Determines whether the current request is hybrid. Optionally, a `Illuminate\Http\Request` instance can be given instead of using the current request. ### Usage ```php if ($this->hybridly->isHybrid()) { // ... } ``` ## `isPartial` * See also: [`is_partial`](./functions.md#is-partial) Determines whether the current request is a [partial reload](../../guide/partial-reloads.md). Optionally, a `Illuminate\Http\Request` instance can be given instead of using the current request. ### Usage ```php if ($this->hybridly->isPartial()) { // ... } ``` ## `share` Shares properties globally across all Hybridly responses. ### Usage ```php $this->hybridly->share('auth.user', fn () => Auth::user()); $this->hybridly->share([ 'app.name' => config('app.name'), ]); ``` ## `flush` Clears all currently shared global properties. ### Usage ```php $this->hybridly->flush(); ``` ## `persist` Marks one or more property paths as persisted so they are always included, even in partial responses. ### Usage ```php $this->hybridly->persist([ 'auth', 'flash', ]); ``` ## `resolveVersionUsing` Sets a callback used to resolve the asset version for responses. ### Usage ```php $this->hybridly->resolveVersionUsing(fn () => md5_file(public_path('build/manifest.json'))); ``` ## `setUrlResolver` Defines how the URL is resolved for the next response. ### Usage ```php use Illuminate\Http\Request; $this->hybridly->setUrlResolver( fn (Request $request) => $request->fullUrl(), ); ``` ## `getUrlResolver` Returns the currently configured URL resolver callback. ### Usage ```php $resolver = $this->hybridly->getUrlResolver(); ``` ## `renderExceptionsInDevelopment` Ensures Hybridly exception rendering is enabled in both `production` and `local` environments. ### Usage ```php $this->hybridly->renderExceptionsInDevelopment(); ``` ## `renderExceptionsUsing` Defines the response that should be rendered when a handled exception occurs. ### Usage ```php use Illuminate\Http\Request; use Throwable; $this->hybridly->renderExceptionsUsing( fn (Throwable $exception, Request $request) => $this->hybridly->view('errors.server-error', [ 'message' => $exception->getMessage(), ]), ); ``` ## `handleSessionExpirationUsing` Defines the response that should be returned when a `419` session expiration occurs. ### Usage ```php $this->hybridly->handleSessionExpirationUsing( fn () => redirect() ->route('login') ->with('error', 'Your session expired. Please login again.'), ); ``` --- --- url: /releases/v0.6.0.md --- # Hybridly v0.6.0 ## Upgrading You may upgrade Hybridly using the following commands: ```shell [pnpm] composer require "hybridly/laravel:^0.6.0" pnpm i -D hybridly@^0.6.0 ``` This release contains breaking change. Please follow the [upgrade guide](../guide/upgrade/v0.6.x.md). ## The development server prompt is improved The Laravel integration is now built-in instead of relying on the official `laravel/vite-plugin` package, which allows us to update the prompt to something more useful. ![New prompt](v0.6.0/new-prompt.webp) Additionally, opening the development server URL will now redirect to the actual application URL. In the example above, `https://demo.test:5173` redirects to `https://bluebird.test`. ## Local builds show a warning Some people prefer working without the development server by default, starting it only when needed. In this situation, they have a local build of the application. This is easy to forget and can lead to frustration when debugging why changes made to the front-end are not reflected, so this release introduces a warning when the application has been built with `APP_ENV` set to `local`. ![Local build](v0.6.0/local-build.webp) Hovering the notification will lower its opacity, and clicking on it will close it. You may disable it completely by setting `warnOnLocalBuilds` to `false` in `vite.config.ts`. ## New `loadModule` architecture API The architecture API has a [`loadModuleFrom`](../api/laravel/hybridly.md#loadmodulefrom) method that loads namespaced views, layouts and components from the specified directory. Since modules were loaded from the same directory as where the method was called most of the time, this release adds a [`loadModule`](../api/laravel/hybridly.md#loadmodule) alternative that does just that. ## The `@vite` directive no longer needs the entrypoint path The application entrypoint, `resources/application/main.ts` by default, is configurable in `config/hybridly.php`. It felt bad having to repeat it in multiple places, so Hybridly now overrides the `@vite` directive to automatically specify this path. Specifying your own files in the directive and in `vite.config.ts` is still supported. ## Vite 5 is supported Vite 5 is now supported. Their CommonJS API [has been deprecated](https://vitejs.dev/guide/troubleshooting.html#vite-cjs-node-api-deprecated), so it is now even more recommended to convert your project to ESM if it is not using it. Vite 5 introduced a breaking change for back-end integrations: the `manifest.json` file moved to a `.vite` subdirectory. Hybridly reverts this change, but still supports the new location if you decide to move it back. ## Improved configuration Internally, Hybridly has moved from `config()` calls to a singleton configuration object. This alone doesn't affect projects using it, but we took the opportunity to improve the configuration options, for clarity. Most of this release's breaking changes come from this refactor. You may follow the [upgrade guide](../guide/upgrade/v0.6.x.md) to see what changed. ## `flash` has been removed The Hybridly singleton was including a `flash` method, which implemented a different way of flashing data to the session. In practice, it was not useful, and it was deprecated since a few versions. It has been removed in v0.6.0. --- --- url: /releases/v0.7.0.md --- # Hybridly v0.7.0 ## Upgrading You may upgrade Hybridly using the following commands: ```shell [pnpm] composer require "hybridly/laravel:^0.7.0" pnpm i -D hybridly@^0.7.0 ``` This release contains breaking change. Please follow the [upgrade guide](../guide/upgrade/v0.7.x.md). ## Data has been upgraded to version 4 There are a few breaking changes, especially related to collections. You may read [Ruben Van Assche's blog post](https://rubenvanassche.com/hi-there-laravel-data-4/) about it and check out the [v4 documentation](https://spatie.be/docs/laravel-data/v4/introduction). Here is an example upgrade that you may need to do in your application: ```php return hybridly('chirps.index', [ 'chirps' => ChirpData::collect($chirps, PaginatedDataCollection::class)->transform(), // [!code ++] 'chirps' => ChirpData::collection($chirps), // [!code --] ]); ``` Note the `transform` call above, which is related to a [bug](https://github.com/spatie/laravel-data/pull/682) that might be fixed later. ## `defineLayout` and `defineLayoutProperties` are gone Previously, you had the ability to define a page's layout and its properties through the `defineLayout` and `defineLayoutProperties` utils. These functions were implemented prior to [`defineOptions`](https://vuejs.org/api/sfc-script-setup.html#defineoptions) being added to Vue. Even though they were useful, there was multiple issues with them: * They caused a recursive page refresh in some situations, resulting in a lag after HMR, * They were incompatible with server-side rendering, * Their implementation was somewhat complex to maintain due to reactivity hoops. You may now use `defineOptions`: ```ts defineOptions({ layout: main, properties: { fullscreen: false, }, }) ``` ## `useBackForward` now accepts options [`useBackForward`](../api/utils/use-back-forward.md) is an util function that registers callbacks executed during back-forward browser navigations. It now accepts a parameter that defines whether the page component should be reloaded on back-forward navigations: ```ts useBackForward({ reload: true, }) ``` This is the equivalent of calling the returned `reloadOnBackForward` function. It's simply more practical. ## TypeScript files are no longer automatically imported in all loaded directories Previously, TypeScript files present in any loaded directory would be auto-imported using `unplugin-auto-imports`. For instance, when loading a module, any TypeScript file either at the root of the directory or nested inside of it would have its exports auto-imported. This behavior was not easily customizable and would require developers to override the `unplugin-auto-imports` configuration in `vite.config.ts`. In v0.7.0, this behavior has changed. By default, only TypeScript files at the root of a module will be loaded. They can also be ignored by setting the `loadTypeScript` argument to `false`. You may also arbitrarily load TypeScript files in any directory by calling the `loadTypeScriptFilesFrom` method. ## The `php` option is no longer available for the Hybridly plugin The `php` option allowed to pass the path to the PHP executable path to the plugin so it could call the back-end and fetch the configuration needed by Hybridly to run. However, this is incompatible with a new feature that allows to pass the options as a callback which accepts the loaded configuration. Additionnally, it makes more sense to define the PHP path as an environment variable, because it is something specific to an environment. ## `useContext` is gone `useContext` was a function that returned a `computed` version of Hybridly's context object. Prior to Vue 3.4, this was a reactive object—however, memoization changes in Vue 3.4 [broke that behavior](https://github.com/vuejs/core/issues/10046). Since this utility function was meant to be a escape-hatch though, we decided to remove it in favor to [`getRouterContext`](../api/utils/get-router-context.md), a similar function that already exists in Hybridly core. ## Initial payload is available during initialization The initial payload that contains data from the back-end during the initial page render is now available to the `setup` callback from [`initializeHybridly`](../api/utils/initialize-hybridly.md#setup) for advanced usage. ## Hidden table actions can now be invoked A bug was preventing dynamically-hidden table actions from being invoked. This issue has been fixed by [Owen Conti](https://github.com/hybridly/hybridly/pull/112). ## Model resolution can be customized for table actions [Owen Conti](https://github.com/hybridly/hybridly/pull/113) contributed a way to customize the logic for resolving models when working with table actions. This is specifically useful to handle scenarios where specific scopes are needed, such as when using soft-deleted models. ## Vue files identifiers are now lower-case Vue files loaded using [`loadModule`](../api/laravel/hybridly.md#loadmodule) would have their identifier generated using the file structure. For instance, the identifier for the following `show.vue` file would be `fleet-tracking::Synthesis.show`: ``` src/ └── Feature/ └── FleetTracking/ ├── FleetTrackingServiceProvider.php └── Synthesis/ └── show.vue ``` In v0.7, the same file would be identified by `fleet-tracking::synthesis.show`. ## Fixed downloading of binary files In a previous version, Hybridly started supporting the download of files using its router. This is a nice quality of life feature, but it was not working when handling binary file types. This was due to Axios not allowing the response type to be changed after the request was initiated. This is now fixed by handling every response using array buffers and converting them to JSON or blobs using an interceptor. ## `base` supports IDE autocompletion [@brampkg](https://twitter.com/brampkg) contributed IDE autocompletion support for the [`base`](../api/laravel/hybridly.md#base) method through Laravel Idea. --- --- url: /guide/installation.md --- # Installation Hybridly consists of a Laravel adapter, a Vue adapter and a Vite plugin. These last two are distributed together in the `hybridly` npm package. The simplest way to get started with Hybridly is to use the preset in a fresh Laravel project. Alternatively, you can proceed to a manual installation following this guide. ## Preset The recommended way of installing Hybridly is to use the preset in a [fresh Laravel project](https://laravel.com/docs/installation). Run the following command in the root of your project: :::code-group ```bash [bun] bunx @preset/cli apply hybridly/preset ``` ```bash [pnpm] pnpm dlx @preset/cli apply hybridly/preset ``` ```bash [yarn] yarn dlx @preset/cli apply hybridly/preset ``` ```bash [npm] npx @preset/cli apply hybridly/preset ``` ::: The preset automatically sets up [Tailwind CSS](https://tailwindcss.com) and [Pest](https://pestphp.com). You may add any of the following flags to the previous command to customize the preset: | Flag | Description | | ------------- | ------------------------------------------------------------------------------------- | | `--i18n` | Install and setup [**vue-i18n**](https://vue-i18n.intlify.dev/) | | `--no-pest` | Do not setup [**Pest**](https://pestphp.com/) | | `--no-strict` | Do not setup Laravel strict mode | | `--no-ide` | Do not setup [**laravel-ide-helper**](https://github.com/barryvdh/laravel-ide-helper) | More information about the preset can be found on its [**repository**](https://github.com/hybridly/preset). :::info Installation Once you have installed the preset, **you do not need to follow the rest of the installation guide**. Hybridly is already installed. ::: ## Server-side setup This section is a summary of what's needed server-side, so that you can conveniently copy-paste snippets. For more thorough explanations, follow the [detailed guide](#detailed-installation-guide). ### Install the Laravel package ```bash composer require hybridly/laravel ``` ### Create `root.blade.php` in `resources` :::code-group ```html [resources/root.blade.php] @vite @hybridly ``` ::: ## Use `HandleHybridRequests` and return hybrid views :::code-group ```php [app/Users/ShowUserController.php] use App\Users\User; use function Hybridly\view; final readonly class ShowUserController { public function __invoke(User $user): HybridResponse { return view('users.show', [ 'user' => $user ]); } } ``` ```php [routes/web.php] Route::get('/users/{user}', ShowUserController::class) ->middleware(HandleHybridRequests::class) // [!code hl] ->name('users.show'); ``` ::: ## Client-side setup This section is a summary of what's needed client-side, so that you can conveniently copy-paste snippets. For more thorough explanations, follow the [detailed guide](#detailed-installation-guide). ### Install the dependencies :::code-group ```bash [ni] ni hybridly vue -D ``` ```bash [bun] bun i hybridly vue -D ``` ```bash [pnpm] pnpm i hybridly vue -D ``` ```bash [npm] npm i hybridly vue -D ``` ```bash [yarn] yarn add hybridly vue -D ``` ::: ### Configure Vite Rename `vite.config.js` to `vite.config.ts`, and register `hybridly/vite` as a plugin. You may uninstall `@laravel/vite-plugin`, which functionality is included in the Hybridly plugin, as well as `axios`. :::code-group ```ts [vite.config.ts] import tailwindcss from '@tailwindcss/vite' import hybridly from 'hybridly/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ hybridly(), tailwindcss(), ], }) ``` ::: ### Initialize Hybridly Delete the `resources/js` and `resources/css` directories, then create `resources/main.ts` and `resources/main.ts` with the following snippet: :::code-group ```ts [resources/main.ts] import { initializeHybridly } from 'virtual:hybridly/config' import './main.css' initializeHybridly() ``` ```css [resources/main.css] @import "tailwindcss"; ``` ::: Use [`enhanceVue`](../api/utils/initialize-hybridly.md#enhancevue) to register plugins, components or directives. ### Add a `tsconfig.json` ```json { "files": [], "references": [ { "path": "./.hybridly/tsconfig.json" }, { "path": "./.hybridly/tsconfig.node.json" } ] } ``` Note that until the development server is started, this may cause errors in your editor, as the file it references does not exist yet. ## Detailed installation guide This little guide assumes you are using macOS with [Laravel Valet](https://laravel.com/docs/13.x/valet) or [Herd](https://laravel.com/docs/13.x/installation#installation-using-herd). ### Create and `cd` into the project Create the project using `composer create-project` or the Laravel installer, navigate to the project directory and install front-end dependencies. ```bash composer create-project laravel/laravel hybridly-app cd hybridly-app bun install ``` At this point, you probably want to initialize your repository and create a first commit. ```bash git init git add . git commit -m "chore: initialize project" ``` ### Install the Laravel adapter The first step is to install the Laravel adapter. ```bash composer require hybridly/laravel ``` ### Create `root.blade.php` Hybridly needs a `root.blade.php` file that will be loaded on the initial page load. The name and path are configurable, but it's a good default. Proceed to delete everyting in `resources`, and create `resources/root.blade.php`. It needs to contain the `@vite` directive and to load assets, as well as the `@hybridly` directive to create the element that will host the Vue application. ```html @vite @hybridly ``` ### Install the Vite plugin The next step is to install and register the Vite plugin. It is, along with the Vue adapter, distributed in the `hybridly` package on [npm](https://npmx.dev/package/hybridly). We also need to install `vue`, and uninstall `axios`, which is not needed. ```bash bun install hybridly vue -D bun uninstall axios ``` At this point, your `package.json` should look like the following: ```json { "$schema": "https://www.schemastore.org/package.json", "private": true, "type": "module", "scripts": { "build": "vite build", "dev": "vite" }, "devDependencies": { "@tailwindcss/vite": "^4.0.0", "axios": ">=1.11.0 <=1.14.0", "concurrently": "^9.0.1", "hybridly": "^0.9.0", "laravel-vite-plugin": "^3.0.0", "tailwindcss": "^4.0.0", "vite": "^8.0.0", "vue": "^3.5.32" } } ``` Now is a good time to rename `vite.config.js` to `vite.config.ts` (because we like types) and register the `hybridly` plugin, exported by `hybridly/vite`. The `laravel-vite-plugin` can be removed and uninstalled. ```ts import hybridly from 'hybridly/vite' // [!code ++] import laravel from 'laravel-vite-plugin' // [!code --] import { defineConfig } from 'vite' export default defineConfig({ plugins: [ laravel({ // [!code --] input: ['resources/css/app.css', 'resources/js/app.js'], // [!code --] refresh: true, // [!code --] }), // [!code --] hybridly(), // [!code ++] ], }) ``` ### Initialize Hybridly The Vite part is done, now you need to make your scripts aware of Hybridly. Create `resources/main.ts`, which should contain the following snippet: ```ts import { initializeHybridly } from 'virtual:hybridly/config' initializeHybridly({ enhanceVue: (vue) => {}, }) ``` You can read more about `initializeHybridly` in the [API documentation](../api/utils/initialize-hybridly.md). The [`enhanceVue`](../api/utils/initialize-hybridly.md#enhancevue) property is optional, but you will most likely need it to register plugins such as [`unhead`](https://unhead.unjs.io/docs/vue/head/guides/get-started/installation). You may also have a type error on the `virtual:hybridly/config` import, which is going to be away in the next section. ### Add a `tsconfig.json` Your project needs a `tsconfig.json` file to understand objects such as `import.meta` or imports like `virtual:hybridly/config`. The complicated part of this file is automatically generated by Hybridly when the development server is started, so you can create your own simple `tsconfig.json` that references it: ```json { "files": [], "references": [ { "path": "./.hybridly/tsconfig.json" }, { "path": "./.hybridly/tsconfig.node.json" } ] } ``` The [TypeScript documentation](https://www.typescriptlang.org/tsconfig) is a good place to understand the configuration options that are available in this file. ### Ignore `.hybridly` Add `.hybridly` to your `.gitignore`. This directory will be generated by the development server and contain diverse files such as the main `tsconfig.json`, localization files, and diverse TypeScript declaration files. ### Create a view component It's almost done. We just need a view component and a route to serve it. Create a `resources/index.view.vue` file. A view component is a standard Vue component, except it's now a view component. ```vue ``` ### Create a route In `routes/web.php`, you can now instruct a route to load your new view component. You just need to give its name to Hybridly's [`view`](../api/laravel/functions.md#view) function. ```php use function Hybridly\view; Route::get('/', function () { return view('index'); }); ``` ### Configure `.env` If not already done, copy `.env.example` to `.env` and configure it. Run `php artisan key:generate` and, more importantly, update `APP_URL` to the URL that will serve your application. Hybridly requires it to be accurate to generate URLs. If you use `php artisan serve`, set it to `http://127.0.0.1:8000`. Otherwise, refer to the documentation of your web server. ### Start the development server That's it. You only need to run `bun run dev` and open the application in your browser. ```shell composer dev open http://127.0.0.1:8000 ``` ### Troubleshooting In some cases, you might see a blank page. To fix it, open the URL displayed in your browser console in a separate tab, and accept its certificate. ## What's next? You may want to look into setting up [PHP to TypeScript transformation](../guide/typescript.md). To get started with building your application, you should read how to [pass data from the server](../guide/responses.md) to the front-end or how to [navigate between views](../guide/navigation.md). Feel free to explore the whole documentation before committing to Hybridly. Have fun building your applications! --- --- url: /guide/i18n.md --- # Internationalization ## Overview While Hybridly does not have much to do with internationalization, it provides an artisan command for converting Laravel lang files to JSON files that can be consumed by [`vue-18n`](https://vue-i18n.intlify.dev/). Note that this feature will move to a separated package at some point. :::info Internationalization keys [This article](https://phrase.com/blog/posts/ruby-lessons-learned-naming-and-managing-rails-i18n-keys/) is a good resource regarding internationalization key management. ::: ## Usage with `vue-i18n` The translation files generated by Hybridly are compatible with [`vue-i18n`](https://vue-i18n.intlify.dev/), which is the de-facto solution for translating Vue applications. Its setup is rather trivial, but to globally benefit from TypeScript support, it needs to be configured by extending the [`DefineLocaleMessage`](https://vue-i18n.intlify.dev/guide/advanced/typescript.html#global-resource-schema-type-definition) interface. The following file demonstrates how to do this: :::code-group ```ts [resources/i18n.ts] import messages from '#/locales.json' import { createI18n } from 'vue-i18n' export type MessageSchema = typeof messages['en'] declare module 'vue-i18n' { export interface DefineLocaleMessage extends MessageSchema {} } const i18n = createI18n<[MessageSchema], 'en' | 'fr'>({ locale: 'en', messages, }) export default i18n ``` ::: This file can then be imported and registered as a Vue plugin: ```ts import { initializeHybridly } from 'virtual:hybridly/config' import i18n from './i18n' initializeHybridly({ enhanceVue: (vue) => vue.use(i18n), }) ``` ## Configuration `config/hybridly.php` allows for a few configuration options. ```php return [ // ... 'i18n' => [ 'file_name_template' => '{locale}.json', 'file_name' => 'locales.json', ] ]; ``` * **file\_name\_template**: name template for the the individual locale files. * **file\_name**: name of the file that contain all translations. ## Manual command usage Running `php artisan hybridly:i18n` will create a `.hybridly/locales.json` file containing all locales. Optionally, it is possible to extract each locale in its own file by using the `--locales` option. :::tip Automatic re-generation Note that, by default, translation files are automatically be re-generated when a change to a lang file is made. This is done by `vite-plugin-run`, which is [automatically incuded](../configuration/vite.md#run). ::: --- --- url: /guide.md --- # Introduction ## Overview Using a protocol similar to the one [Jonathan Reinink](https://reinink.ca) invented for [Inertia](https://inertiajs.com), Hybridly makes it possible to build applications using Vue instead of Blade, while keeping the benefits of classic monolithic applications. Hybridly is essentially very similar to Inertia, but it has a different philosophy. Since it focuses on Laravel, Vite and Vue instead of being completely framework-agnostic, it **has more built-in features** and **quality of life improvements**, which results in a **better developer experience** overall. In other words, Hybridly is more like a framework built on top of Laravel and Vue, focusing specifically on being the perfect glue between the two. ## What it looks like Working with Hybridly is pretty similar to working with basic Laravel. The main difference is how you render views, since Hybridly uses Vue. Due to the nature of client-rendered applications, you also lose the access of some features like [`route`](https://laravel.com/docs/11.x/urls#urls-for-named-routes) or [`@can`](https://laravel.com/docs/11.x/authorization#via-blade-templates) in templates. Fortunately, we have alternatives for them! Below are some basic examples of what Hybridly code looks like: ### Controllers Controllers look the same as what you are used to with Laravel. The main difference is that you return [hybrid responses](./responses.md) using the [`Hybridly\view`](../api/laravel/functions.md#view) function instead of Laravel's built-in `view`. :::code-group ```php [UserProfileController.php] use App\Users\UserData; use App\Users\User; use App\Users\UpdateUserRequest; use Hybridly\Contrats\HybridResponse; use function Hybridly\view; final readonly class UserProfileController { public function show(User $user): HybridResponse { return view('users.show', [ 'user' => UserData::from($user) ]); } public function update(User $user, UpdateUserRequest $request): RedirectResponse { $user->update($request->validated()); return back()->with('success', 'Changes saved.'); } } ``` ```php [routes.php] Route::get('/users/{user}', [UserProfileController::class, 'show']) ->name('users.show'); Route::put('/users/{user}/update', [UserProfileController::class, 'update']) ->name('users.update'); ``` ::: ### Templates Hybridly uses [Vue](https://vuejs.org) to render pages using single-file components. You may use [layouts](./views-and-layouts.md#layouts) using a simple directive. ```vue [show.vue] ``` There are a few things going on there: * The `template` block has a `layout` directive that specifies which persistent layout is being used for this page. Learn more about [layouts](./views-and-layouts.md#persistent-layouts). * The `user` property is typed using an auto-generated interface from a data object. You can learn more about TypeScript integration [here](./typescript.md). * We use the [``](../api//components/form.md) component or the [`useForm`](../api/utils/use-form.md) util to work with forms. Learn more about them on the [forms documentation](./forms.md). * Hybridly provides a [`route`](../api/utils/route.md) util to generate URLs, similar to Laravel's [`route`](https://laravel.com/docs/11.x/urls#urls-for-named-routes) helper. ### Beyonds the basics Rendering a single page with a form is cool, but real-world applications are more complex. After learning about essential features using the sidebar to your left, you may want to learn about: * [How to render dialogs](./dialogs.md) * [How to implement filters and sorts](./refining.md) * [How to implement data tables](./tables.md) ## About Inertia and Hybridly I was barely into the Laravel ecosystem when Jonathan Reinink was already looking for a way to [build Vue-powered Laravel applications](https://reinink.ca/articles/server-side-apps-with-client-side-rendering) the right way. He came up with Inertia, which is now backed by Laravel. It powers [Forge](https://forge.laravel.com) and [Laravel Cloud](https://cloud.laravel.com). It is a well-established tool. If you already build applications using Inertia and you don't feel like you should change your stack, there is no need to reach for a different tool. **Hybridly came to life because of Inertia's history**. When it was first released, its philosophy was to stay very minimalist—it had very few features beyond basic routing and rendering. Moreover, its pace of development has been a source of frustration for its users, with pull requests and issues not being handled for months. Nowadays, Inertia has changed a lot. It is very featureful, supports TypeScript and is updated regurlarly. However, Hybridly is here to stay. It is what I use for development, and I will keep maintaining it for the foreseeable future. It is a more opinionated tool, with different built-in features and quality of life improvements, the most notable ones being support for [dialogs](./dialogs.md) and [data tables](./tables.md). --- --- url: /configuration/laravel.md --- # Laravel configuration ## Overview Like most Laravel packages, Hybridly has a few settings available for modification in `config/hybridly.php`. The defaults are generally good, but you may change them to your liking. Run the following command to publish the `config/hybridly.php` file. ```bash php artisan vendor:publish --tag=hybridly-config ``` ## Route filters The built-in [route support](../api/utils/route.md) requires Hybridly to share available routes to the front-end. In most cases, this is not an issue, but in some situations you may want to restrict which routes are directly exposed in order to reduce the surface of a potential attack. ### Excluding routes You may add filters to `router.exclude`. These filters are matched against the final URI, and support wildcards: ```php 'router' => [ 'exclude' => [ 'admin*', // [!code ++] ], ], ``` ### Including vendors By default, routes from vendor packages are not available to the front-end. You may include some by specifying the `vendor/package` identifier in the `router.allowed_vendors` option: ```php 'router' => [ 'allowed_vendors' => [ 'laravel/fortify', // [!code ++] ], ], ``` ## Architecture Hybridly is flexible when it comes to architecturing your application. It offers two presets: the default one, a [single-level architecture](../guide/architecture.md#single-level) similar to the architecture that Laravel promotes, and a [modular](../guide/architecture.md#modular) one. ## Specifying a preset You may specify the preset you want to use in the `architecture.preset` option. Possible values are `default` and `modules`. ```php 'architecture' => [ // [!code focus:2] 'preset' => 'default', // [!code hl] 'root' => 'resources', 'eager_load_views' => true, ], // [!code focus] ``` If you want to use a custom architecture, you may disable the presets by setting that option to `false`. To learn how to define your own architecture, read the [corresponding documentation](../guide/architecture.html#custom). ## Updating the root directory By default, Laravel comes with the `resources` directory, that is used by many external packages and Laravel itself. Hybridly also uses `resources` for its default architecture, but you may chose to use a separate directory if you want. To do that, update the `architecture.root` option: ```php 'architecture' => [ // [!code focus] 'preset' => 'default', 'root' => 'frontend', // [!code focus] 'eager_load_views' => true, ], // [!code focus] ``` This directory is used by the `@` import alias and for some of the integrations, like auto-imports, icons, or for loading the `/application/main.ts` entrypoint. ## Eager-loading views By default, Hybridly will eager-load view components, which means that users will have to load all views at once when accessing the application. This is a good default, but if your application has a lot of views, you may need to disable eager-loading, so views can be lazy-loaded as needed by your users. To do that, simply set `architecture.eager_load_views` to `false`. ```php 'architecture' => [ // [!code focus] 'preset' => 'default', 'root' => 'frontend', 'eager_load_views' => false, // [!code focus] ], // [!code focus] ``` --- --- url: /api/router/navigation.md --- # Navigation The `router` object contains a few utils that can be used to programmatically navigate through the application. Most of the navigation functions accept an `options` argument whose properties are documented in the [router options](./options.md) page. When a `url` argument is accepted, its type is the same as the `url` property of the `options` argument. ## `get`, `post`, `put`, `patch`, `delete` This function initiates a programmatic navigation to the given URL using the corresponding method. When performing non-`GET` requests, by default, the `preserveState` option will be set to `true`. ```ts router.get(url, options) router.post(url, options) router.put(url, options) router.patch(url, options) router.delete(url, options) ``` ## `to` This function initiates a request to a named route. The HTTP method is inferred from the route definition and can still be overridden in options. ```ts router.to(name, parameters, options) ``` ## `reload` This function initiates a request to the current URL. By default, it preserves state and scroll and runs in `async` mode, which is ideal for background refreshes. ```ts router.reload(options) ``` ## `local` This function initiates a local-only navigation. This navigation will not reach the server — it will only re-render the specified (or current) component with the specified properties. ```ts router.local(url, options) ``` Its `options` argument is different from the other navigation functions: only the `replace`, `preserveScroll` and `preserveState` options are available. Additionally, a `component` and a `properties` options are also available. ```ts interface ComponentNavigationOptions { /** Name of the component to use. */ component?: string /** Properties to apply to the component. */ properties?: Properties } ``` :::info Global properties Note that, because of roundtrip-less nature, global properties will not be updated when using this method. ::: ## `external` This function initiates an external, hard navigation. This is an equivalent of using `document.location.href`. If provided, the `data` object will be converted to query parameters. ```ts router.external(url, data) ``` ## `navigate` This function initiates a programmatic navigation without any specific default. ```ts router.navigate(options) ``` ## `abort` This function aborts the current request. The `abort` hook will be triggered. ## `current` This function returns the current route name, if defined. ## `matches` This function returns `true` if the given route name matches the current route name. Optionally, you may pass parameters to match more specific routes. ```ts router.matches('tenant.*') router.matches('profile', { user: currentUserId }) ``` ## `dialog.close` This function closes the current dialog. It takes the same options as the other router functions, as well as a `local` option that indicates whether a round-trip to the server will be made to update the base view's properties. ```ts router.dialog.close() ``` ## `history.get` This function fetches the given key from the history state. The state is specific to the current [history entry](https://developer.mozilla.org/en-US/docs/Web/API/History). ```ts router.history.get(key) ``` ## `history.remember` This function sets the given key and value in the history state. The state is specific to the current [history entry](https://developer.mozilla.org/en-US/docs/Web/API/History). ```ts router.history.remember(key, value) ``` --- --- url: /guide/navigation.md --- # Navigation ## Overview Because Hybridly creates a single-page application, a special navigation needs to be made to avoid reloading the whole framework to load a page. This is done using the [``](../api/components/router-link.md) component, or programmatically by using the [routing API](../api/router/navigation.md). ## Using the component [``](../api/components/router-link.md) is a simple component that acts as an anchor tag, except it catches navigations to transform them into hybrid requests. ```vue ``` Learn more about the options available on its [API documentation](../api/components/router-link). ## Programmatically In many cases, such as when submitting forms, handling refreshes from WebSocket, or for any other reason, you will need to trigger navigations programmatically. This is done using the [`router` navigation API](../api/router/navigation). ```ts router.to(route, parameters, options) router.reload(options) router.get(url, options) router.post(url, options) router.put(url, options) router.patch(url, options) router.delete(url, options) router.external(url, options) router.navigate(options) ``` Learn more about the functions and options available in their [API documentation](../api/router/navigation). ## Asynchronous requests There can only be one navigation active at a time. When attempting a navigation while another is still pending, the previous one will be cancelled. However, it is possible to make an asynchronous request that will not be prevented by a subsequent navigation, which is particularly useful to load or refresh data transparently. You may read more about this on the [partial reloads](./partial-reloads.md) documentation. --- --- url: /guide/partial-reloads.md --- # Partial reloads ## Overview Partial reloads are repeated requests to the same page which purpose is to update or fetch specific properties, but not all of them. As an example, consider a page that includes a list of users, as well as an option to filter the users by their company. On the first request to the page, both the `users` and `companies` properties are passed to the view component. However, on subsequent requests to the same page—maybe to filter the users—you can request only the `users` property from the server, and not the `companies` one. ## Making partial reloads Partial reloads only work for requests made for the same page, as their purpose is to update the current properties or fetch new ones. Hybrid requests are "partial" when the [`only`](../api/router/options.md#only) or [`except`](../api/router/options.md#except) option is used: ```ts // Refreshes only the `users` property router.reload({ only: ['users'] }) // ...or refresh everything except the `companies` property router.reload({ except: ['companies'] }) // ...or refresh only a nested property router.reload({ only: ['user.full_name'] }) ``` ### Asynchronous requests Partial reloads are "asynchronous": it means that they can be triggered in parallel, don't show a progress bar, and replace the current history entry instead of pushing a new one. Under the hood, partial requests set [`mode`](../api/router/options.md#mode) to `async`: ```ts router.get({ only: ['users'], mode: 'async', // [!code hl] }) ``` #### Cancelling and interrupting async requests When multiple asynchronous requests can overlap, you may control their interruption behavior. ```ts router.reload({ only: ['notifications'], mode: 'async', group: 'navbar', interruptAsyncOnStart: 'same-group', cancelOnNavigation: true, }) ``` ##### `group` This option allows for grouping asynchronous request together. This setting interracts with other requests in the same group that use `interruptAsyncOnStart`. ##### `interruptAsyncOnStart` This option configures how that request should interrupt other in-flight asynchronous requests. * `none` (default) — does not interrupt any request. * `same-group` — interrupts requests that share the same `group`. * `all` — interrupts all requests. ##### `cancelOnNavigation` This option configures whether the request should be cancelled when a full navigation starts. This is useful to prevent race conditions. ## Persistent properties When using partial reloads, any non-specified property will not be sent back to the front-end. This is the desired behavior, except when it comes to data that is needed no matter the context. For instance, you may initiate a partial reload that triggers a flash message, which needs to be returned with the partial reload's response even if the `flash` property was not included in its `only` parameter. To solve this problem, persistent properties will always be sent, even in partial reloads. ### Persisting properties To mark a property as persisted, you may use the `persist()` method on the `Hybridly` instance: ```php hybridly()->persist('toasts'); ``` This is typically done in a dedicated middleware, or sometimes in a service provider. #### Example In the following example, the `ShareToasts` is included in every request. When the session contains `toasts` instances, they are shared to the client. Since the middleware also persist the `toasts` property, it will always be included, even in partial requests. :::code-group ```php [src/Toasts/ShareToasts.php] final readonly class ShareToasts { public function __construct( private Hybridly $hybridly, ) {} public function __invoke(Request $request, Closure $next): Response { $toasts = []; foreach ($request->session()->get('toasts', default: []) as $toast) { if (! $toast instanceof Toast) { continue; } $toasts[] = $toast; } $this->hybridly->persist('toasts'); $this->hybridly->share('toasts', $toasts); return $next($request); } } ``` ```ts [src/Toasts/toasts.d.ts] import 'hybridly' declare module 'hybridly' { export interface GlobalHybridlyProperties { toasts: App.Toasts.Toast[] } } export {} ``` ::: ## On-demand properties In situations where some data is not needed when the page initially loads, you may use on-demand properties. These properties are not evaluated until specifically required by a partial request. To achieve this, you can use the [`on_demand`](../api/laravel/functions.md#on_demand) function: ```php use function Hybridly\on_demand; return views('users.show', [ 'users' => $users, 'companies' => on_demand(fn () => Companies::query()->all()), ]); ``` ## Deferred properties For performance reasons, it may be desirable to load the page first, and then load other properties which would have slowed down the initial page load. This can increase the perceived performance of the page and improve the user experience. When rendering a view with a [`deferred`](../api/laravel/functions.md#deferred) property, it will not be sent on the initial page load. When the page has loaded, Hybridly will automatically trigger partial reloads for those properties. ```php use function Hybridly\deferred; return view('users.show', [ 'companies' => deferred(fn () => Company::query()->all()) ]) ``` This is functionally the same as manually making a partial reload in the `onMounted` hook: ```ts defineProps<{ companies?: App.Companies.Company[] }>() onMounted(() => { router.reload({ only: ['companies'] }) }) ``` ### Grouping requests By default, all deferred properties are fetched in a single request. When deferring multiple properties that take time to load, it can be faster to parallelize their loading by triggering multiple requests, one for each property. ```php return view('users.show', [ 'chart' => deferred(fn () => $chart), 'stats' => deferred(fn () => $stats, group: 'stats'), ]) ``` ## Mergeable properties By default, incoming properties replace the previous value on the front-end. You may instruct Hybridly to merge the new value into its existing one instead by using the [`merge`](../api/laravel/functions.md#merge) function: ```php use function Hybridly\merge; return view('feed.index', [ 'items' => merge(FeedItemData::collection($items)), ]); ``` ### Prepending By default, merging appends new items to the existing array. You can prepend new items instead by passing `prepend: true`: ```php return view('feed.index', [ 'items' => merge( value: fn () => FeedItemData::collection($items), prepend: true ), ]); ``` ### Deduplicating If some values are meant to be unique, you may deduplicate items by specifying a unique key with the `uniqueBy` option: ```php return view('feed.index', [ 'items' => merge( value: fn () => FeedItemData::collection($items), uniqueBy: 'meta.id' ), ]); ``` ### Specifying merge paths By default, the root value is merged. When working with paginators or other nested data structures, you may specify the paths to merge instead: ```php return view('feed.index', [ 'items' => merge( value: fn () => FeedItemData::collection($items), paths: ['data.items'] ), ]); ``` ### Clearing merged properties You may instruct the server to reset the property instead of merging it. This is useful, for instance, when changing filters. ```ts router.reload({ reset: ['items'] }) ``` ## Lazy evaluation It is possible to delay the evaluation of a property by using a closure. This property will still be evaluated on first page load and subsequent hybrid requests, but **only when the response is actually being sent**. The main ```php hybridly()->share([ 'user' => fn () => [ 'email' => Auth::user()->email, ], ]); ``` --- --- url: /guide/plugins.md --- # Plugins ## Overview Hybridly provides a simple plugin mechanism that allows for globally hooking into its lifecycle events. This is what powers the built-in [progress indicator](./progress-indicator.md). This is the equivalent of Inertia's [global events](https://inertiajs.com/events), except it provides a structured way to extend its functionalities. ## Registering plugins A plugin can be registered through the `plugins` property of the `initializeHybridly` function. ```ts import { MyPlugin } from 'hybridly-plugin-something' // [!code focus] initializeHybridly({ plugins: [ // [!code focus:3] MyPlugin() ], }) ``` ## Developing plugins A plugin is a simple object with at least a `name` property. For convenience, Hybridly exports a `definePlugin` function that provides typings for plugins. Aside from hooking into lifecycle events, plugins can detect when Hybridly is initialized through to the `initialized` hook. ```ts import { definePlugin } from 'hybridly' export default function MyPlugin(options?: MyPluginOptions) { return definePlugin({ name: 'hybridly:my-plugin', initialized(context) { console.log('Hybridly has been initialized') }, // Other lifecycle hooks }) } export interface MyPluginOptions { // ... } ``` ## Plugin-specific hooks In addition to the [request lifecycle events](../guide/hooks.md#request-hooks), the following hooks can be registered in plugins. They may also be awaited if necessary. ```ts export interface Hooks extends RequestHooks { /* [!code focus:28] */ /** * Called when Hybridly's context is initialized. */ initialized: (context: InternalRouterContext) => MaybePromise /** * Called after Hybridly's initial load. */ ready: (context: InternalRouterContext) => MaybePromise /** * Called when a back-forward navigation occurs. */ backForward: (state: any, context: InternalRouterContext) => MaybePromise /** * Called when a component navigation is being made. */ navigating: (options: InternalNavigationOptions, context: InternalRouterContext) => MaybePromise /** * Called when a component has been navigated to. */ navigated: (options: InternalNavigationOptions, context: InternalRouterContext) => MaybePromise /** * Called when a component has been navigated to and was mounted by the adapter. */ mounted: (options: InternalNavigationOptions & MountedHookOptions, context: InternalRouterContext) => MaybePromise } ``` --- --- url: /guide/progress-indicator.md --- # Progress indicator ## Overview Single-page applications, because of their nature, do not benefit from the browser loading indicator. When pages take time to load, it can look like a navigation did not work, whereas it is in progress in the background. To solve this, a progress indicator is required. Hybridly comes with one by default, but you can disable it and implement your own. ## Using the built-in indicator By default, a progress indicator is shown when a request takes longer than 250 milliseconds to finish. You may customize this behavior by providing a `progress` property to the `initializeHybridly` function. ```ts initializeHybridly({ progress: { // The default options are as follow: // [!code focus:6] color: '#fca5a5', delay: 250, includeCSS: true, spinner: false, }, }) ``` ## Using a custom indicator Under the hood, the built-in progress indicator is actually a [plugin](./plugins.md). It hooks into the `start`, `progress`, `validation-error`, `fail` and `after` [lifecycle events](./hooks.md). To build your own custom indicator, disable the built-in one and [create your own plugin](./plugins.md). --- --- url: /guide/refining.md --- # Refining ## Overview Refining is the concept of filtering and sorting data. Hybridly offers a first-party, declarative API for refining queries. The refining process happens as follows: * The available filters and sorts are declared using a `Refine` instance * The `Refine` instance runs the query according to the current request * The query result and the refinements are shared to the view as properties * The view uses [`useRefinements`](../api/utils/use-refinements.md) to generate a user interface and apply sorts and filters ## Refining a query The `Refine` class can be instanciated using a model class name or an Eloquent builder instance. :::code-group ```php [Model] use App\Models\Chirp; use Hybridly\Refining\Refine; Refine::model(Chirp::class); ``` ```php [Eloquent builder] use App\Models\Chirp; use Hybridly\Refining\Refine; Refine::query( Chirp::query()->where('author_id', $user->id) ); ``` ::: This `Refine` instance will update the query according to the specified refiners and the current request. The query can then be executed as usual by chaining any Eloquent builder method, like `paginate` or `get`: ```php $chirps = Refine::model(Chirp::class) ->paginate(); ``` ## Specifying refiners A refiner is an object that updates the query according to the current request. Filters and sorts are example of built-in refiners, but you may create your own by implementing the `Hybridly\Refining\Contracts\Refiner` interface. You may specify refiners for a query using the `with` method: ```php use App\Models\Chirp; use Hybridly\Refining\{Sorts, Filters}; // [!code focus] use Hybridly\Refining\Refine; $chirps = Refine::model(Chirp::class)->with([ // [!code focus:4] Sorts\Sort::make(property: 'created_at', alias: 'date'), Filters\TrashedFilter::make(name: 'trashed'), ]); ``` ## Sharing the query The result of the query can be obtained by calling any valid Eloquent builder method on the `Refine` instance. Similarly, the available refiners can be obtained by calling the `refinements` method. These two objects should be shared as properties to the view: :::code-group ```php [ChirpController.php] public function index() { $this->authorize('viewAny', Chirp::class); $chirps = Refine::model(Chirp::class)->with([ // [!code focus:4] Sorts\Sort::make('created_at', alias: 'date'), Filters\TrashedFilter::make(), ])->forHomePage(); return hybridly('chirps.index', [ // [!code focus:4] 'chirps' => ChirpData::collection($chirps->paginate()), 'refinements' => $chirps->refinements(), ]); } ``` ```vue [index.vue] ``` ::: ## Applying filters and sorts The [`useRefinements`](../api/utils/use-refinements.md) composable may be used to build a user interface that allows applying refiners. It provides methods that can be used to apply or reset filters and sorts, and it has properties that may enumerate available and current refiners. The following example shows how to create a basic user interface using the `filters` properties: ```vue ``` ## Specifying a default sort A sort may automatically get applied in the specified direction by calling the `default` method. ```php Sorts\Sort::make('full_name')->default(); ``` This sort will not be applied if another sort is active. If you wish to always enable it, you may set the `sole` parameter to `false`: ```php Sorts\Sort::make('full_name')->default('asc', sole: false); Sorts\Sort::make('email'); ``` ## Querying nested relationships Filters have basic relationship filtering capabilities, which means you may use the dot-notation syntax to specify a property from a relationship. ```php // ?filters[user]=jon Filters\TextFilter::make('user.full_name', alias: 'user'); ``` It is recommended to specify an alias when filtering relationship properties, otherwise the filter name will have its `.` replaced by underscores. Note that filters using relationship use `whereHas` under the hood, which might not be the best option performance-wise. :::warning Sorts are not supported Note that the provided sorts do not support relationships. You will need to use a custom sort with a subquery to achieve a relationship sort. ::: ## Using an alias It may not be desirable to expose the name of a database column to users. You may use the `alias` argument to specify a name that will identify a refiner: ```php // ?sort=date Sorts\Sort::make('created_at', alias: 'date'); ``` In the example above, `date` is used to apply the sort instead of the column name `created_at`. Note that certain refiners, like `TrashedFilter` or `CallbackFilter`, cannot have an alias as they don't use the specified property in their query. ## Available filters ### `TextFilter` Use `TextFilter` for string columns. ```php Filters\TextFilter::make('full_name'); ``` ### `NumericFilter` Use `NumericFilter` for numeric columns. ```php Filters\NumericFilter::make('price'); ``` ### `DateFilter` Use `DateFilter` for date and datetime values. ```php Filters\DateFilter::make('published_at'); ``` `DateFilter` also supports timeframe filtering: ```php Filters\DateFilter::make('period')->timeframe( start: 'starts_at', end: 'ends_at', ); ``` ### `SelectFilter` This filter will perform a `where` statement on the value provided by the `options` array. If the value is not found in the provided list, the filter will not apply. #### Key-value options If the options provided are a list of key-value pairs, the key represents the query parameter name, and the value for the column name: ```php // ?filters[os]=iphone -> `WHERE os = 'ios'` Filters\SelectFilter::make('os', options: [ 'iphone' => 'ios', 'ipad' => 'ipados', 'samsung' => 'android', ]); ``` #### List options If the option array is a list, the key will be used for both the query parameter and the column name: ```php // ?filters[os]=ios -> `WHERE os = 'ios'` Filters\SelectFilter::make('os', options: [ 'ios', 'ipados', 'android', ]); ``` #### Enum options Alternatively, you may provided a backed enum as the options. ```php use App\Enums\OperatingSystem; // ?filters[os]=ios -> `WHERE os = 'ios'` Filters\SelectFilter::make('os', OperatingSystem::class); ``` ### `TrashedFilter` This filter will include or exclude soft-deleted records: ```php // ?filters[trashed]=only Filters\TrashedFilter::make(); ``` ### `BooleanFilter` This filter will convert the request's value to a boolean value to perform a boolean `where` statement: ```php // ?filters[is_active]=true // ?filters[is_active]=1 // ?filters[is_active]=y Filters\BooleanFilter::make('is_active'); ``` ### `TernaryFilter` Use `TernaryFilter` when you need three states (`true`, `false`, and blank): ```php Filters\TernaryFilter::make('is_active', alias: 'status') ->queries( true: fn (Builder $query) => $query, false: fn (Builder $query) => $query->where('is_active', false), blank: fn (Builder $query) => $query->where('is_active', true), ); ``` ## Available sorts ### `Sort` This sort will sort the records by the specified field: ```php // ?sort=created_at Sorts\Sort::make('created_at'); ``` ## Custom filters and sorts The provided filters are relatively basic and will not suit every situation, notably the ones where relationships are involved. You may use the provided `CallbackFilter` to implement your own filter using a closure or an invokable class: ```php // Invokable class CallbackFilter::make('own_chirps', OwnChirpsFilter::class); // Callback function CallbackFilter::make( name: 'own_chirps', callback: function (Builder $builder, mixed $value, string $property) { $builder->where('author_id', auth()->id()); } ); ``` Similary, the `CallbackSort` class can be used to implement a custom sort: ```php // Invokable class CallbackSort::make('date', DateSort::class); // Callback function CallbackSort::make( name: 'date', callback: function (Builder $builder, string $direction, string $property) { $builder->orderBy('created_at', $direction); } ); ``` --- --- url: /guide/responses.md --- # Responses ## Overview Hybrid responses respect a protocol to which the front-end adapter must adhere. A response contains, among other things, the name of the view component and its properties. To send a response, you would typically use the [`Hybridly\view`](../api/laravel/functions.md#view) function, which renders a view and its properties just like Laravel's own `view` function: ```php use App\Users\User; use App\Users\UserData; final readonly class ShowUserController { public function __invoke(User $user): HybridResponse { Gate::authorize('view', $user); return view('users.show', [ 'user' => UserData::fromModel($user), ]); } } ``` In the example above, the corresponding single-file component would simply accept a `user` property of the type `UserData`: ```vue ``` ## Updating properties It is a common pattern to have a `POST` or `PUT` hybrid request that ends up redirecting back to the previous page, which essentially refreshes the properties of the page to avoid having stale data. ```php public function store(UpdateUserRequest $request): HybridResponse { User::query()->update($request->validate()); return back(); } ``` Such a redirection, though, implies an additional server round-trip and the re-execution of the server-side controller responsible for the view, which might slow down the response, depending on the complexity of the page. If you need a performance boost, you may return only properties from the `POST` or `PUT` controller: ```php use function Hybridly\properties; public function store(UpdateUserRequest $request): HybridResponse { $user = User::query()->update($request->validate()); return properties([ 'user' => $user, ]); } ``` In that situation, the returned properties will be merged with the current ones, similarly to what happens during a [partial reload](./partial-reloads.md). ## Internal redirects When making non-get hybrid requests, you may use redirects to a standard `GET` hybrid endpoint. Hybridly will follow the redirect and update the page accordingly. ```php final readonly class UsersController { public function index(): HybridResponse // [!code focus:8] { $users = User::query()->paginate(); return view('users.index', [ 'users' => UserData::collection($users), ]); } public function store(CreateUserData $data, CreateUser $createUser): RedirectResponse // [!code focus:6] { $createUser->execute($data); return to_route('users.index'); // Redirects to `index` above // [!code hl] } } ``` In the example above, using `router.post('/users', { data: user })` would redirect to the user index page with the updated `users` property. ## External redirects It's often necessary to redirect to an external website, or sometimes even an internal page that doesn't use Hybridly, such as a Filament panel. If you use a classic server-side redirection, the front-end adapter will not understand the response and will display an error modal. Instead, you may use [`Hybridly\to_external_url($url)`](../api/laravel/functions.md#to_external_url) to iniate a client-side redirect using `window.location`: ```php use function Hybridly\to_external_url; to_external_url('https://google.com'); ``` You may also open the URL in a new tab by specifying a target: ```php use Hybridly\Support\Target; use function Hybridly\to_external_url; to_external_url('https://google.com', target: Target::NEW_TAB); ``` ### Potentially non-hybrid requests If you are not sure whether the current request expects a hybrid response, you may still use [`to_external_url`](../api/laravel/functions.md#to_external_url). Under the hood, it will detect if the request is hybrid and use a normal `RedirectResponse` instead if necessary. ## File downloads Download responses using a `Content-Disposition` header are supported. You may use any of the usual utilities for creating downloads, such as [`download`](https://laravel.com/docs/master/responses#file-downloads), [`streamDownload`](https://laravel.com/docs/master/responses#streamed-downloads), or [`Storage::download`](https://laravel.com/docs/10.x/filesystem#downloading-files). ```php return response()->download($invoice->file_path, 'invoice.pdf'); ``` However, [in-browser file responses](https://laravel.com/docs/master/responses#file-responses) are not supported, as there is no way for Hybridly to differentiate it from a normal response. --- --- url: /api/router/options.md --- # Router options Most `router` functions accept an *options* argument for configuring the request. The purpose of each of these options is documented here. ## `url` * Type: `UrlResolvable` The URL to navigate to. Can be a `string`, an [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) object or a [`Location`](https://developer.mozilla.org/en-US/docs/Web/API/Location) object. ## `method` * Type: `GET`, `POST`, `PUT`, `PATCH` or `DELETE` (uppercase or lowercase) HTTP method that will be used for the request. When [uploading files](../../guide/file-uploads.md#limitations), do not use `POST` but instead add a `_method: 'POST'` property to the body of the request. ## `mode` * Type: `'navigation' | 'async'` Defines how the request should behave: * `navigation` performs a full navigation. * `async` performs a background data refresh and, by default, does not show progress and replaces the current history entry. Most `router.reload()` calls use `async` mode by default. ## `data` * Type: `RequestData` Body of the request. Can be or contain a `FormData` object. ## `group` * Type: `string` Optional group identifier for async requests. It is used together with `interruptAsyncOnStart` to define which pending async requests should be interrupted when a new one starts. ## `cancelOnNavigation` * Type: `boolean` When set to `true`, this async request is interrupted when a new navigation request starts. ## `interruptAsyncOnStart` * Type: `'none' | 'all' | 'same-group'` Defines which async requests are interrupted when this request starts: * `none`: interrupt nothing. * `same-group`: interrupt requests in the same `group`. * `all`: interrupt all pending async requests. ## `only` * Type: `string` or `string[]` Defines the properties that will be included in the response. All other properties except the [persistent ones](../../guide/partial-reloads.md#persistent-properties) will be excluded. Read the documentation on [partial reloads](../../guide/partial-reloads.md) for more information. ## `except` * Type: `string` or `string[]` Defines the properties that will be excluded from the response. Specified [persistent properties](../../guide/partial-reloads.md#persistent-properties) will also be excluded. Read the documentation on [partial reloads](../../guide/partial-reloads.md) for more information. ## `reset` * Type: `string` or `string[]` Defines properties that should be cleared before applying incoming data. This is useful when reloading [mergeable properties](../../guide/partial-reloads.md#mergeable-properties) that should be reset instead of merged. ## `preserveState` * Type: `boolean | ((options: NavigationOptions) => boolean)` Defines whether the current view component state should be preserved for this navigation. ## `preserveUrl` * Type: `boolean | ((options: NavigationOptions) => boolean)` Defines whether the current URL should be preserved. This is an advanced option that should not be used often. ## `preserveScroll` * Type: `boolean | ((options: NavigationOptions) => boolean)` Defines whether to preserve the position of the document element's and the scroll regions' scrollbars. Read the documentation on [scroll management](../../guide/scroll-management.md) for more information. ## `replace` * Type: `boolean | ((options: NavigationOptions) => boolean)` Defines whether to replace the current history state instead of adding an entry. This affects the browser's "back" and "forward" behavior. ## `viewTransition` * Type: `boolean | string | string[]` Defines whether to use a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API/Using_types) for the navigation. When a `string` or `string[]` is given, these values are used as transition types. ## `hooks` * Type: `Partial` Defines hooks for the [lifecycle of the request](../../guide/hooks.md#request-lifecycle-events). Read the documentation on [hooks](../../guide/hooks.md) for more information. ## `headers` * Type: `Record` Defines additional headers for the request. ## `errorBag` * Type: `string` Defines the bag in which validation errors will be put. ## `useFormData` * Type: `boolean` When set to `true`, forces the conversion of the `data` option to a `FormData` object. ## `spoof` * Type: `boolean` Automatically [spoofs](https://laravel.com/docs/9.x/routing#form-method-spoofing) the method when submitting a `FormData` with the `PUT`, `PATCH` or `DELETE` method. ## `abortController` * Type: `AbortController` Abort controller used for this request. This can be used to manually cancel a specific request instance. ## `transformUrl` * Type: `UrlTransformable` Object which properties will affect the provided `url`. Can also be a callback receiving the current `URL` and returning a `UrlTransformable` object. ## `progress` * Type: `boolean` When set to `false`, the request will not have a progress bar. Async requests default to `false` unless explicitly enabled. --- --- url: /api/router/response.md --- # Router response Router utils that perform a hybrid request return a promise with an object described below. Note that navigations are asynchronous and awaitable. ```ts interface NavigationResponse { response?: HttpResponse error?: Error } ``` ## `response` * Type: `HttpResponse` Contains the object returned by Hybridly's configured HTTP client. ## `error` * Type: `Error` Contains the error, if there was one. Hybridly exports `isHybridlyError`, `isNavigationCancelledError`, `isHttpAbortError` and `isHttpError` type guards to help you determine the type of error. --- --- url: /guide/routing.md --- # Routing ## Overview Hybridly, as opposed to single-page application, does not require redefining routes on the front-end. You only need to register routes and their controller actions as you would do in a normal Laravel application. The only catch is to return a hybrid view instead of a Blade view. This is done by using the [`Hybridly\view`](../api/laravel/functions.md#view) function: ```php use App\Data\UserData; use App\Models\User; use function Hybridly\view; // [!code hl] final readonly class ShowUserController { public function __invoke(User $user) { return view('users.show', [ // [!code hl] 'user' => UserData::from($user) ]); } } ``` Learn more about sending responses in [their documentation](./responses.md). ## Generating URLs Since views are written in single-file components, global Laravel or PHP functions like `route` are not available. Instead, you may use Hybridly's [`route`](../api/utils/route) util. This function is typed, which means you get TypeScript autocompletion and static analysis. :::code-group ```php [routes/web.php] Route::get('/', ShowIndexController::class) ->name('index') Route::get('/users/{user}', [UsersController::class, 'show']) ->name('users.show') ``` ```vue [resources/index.view.vue] ``` ```ts [resources/util.ts] import { route } from 'hybridly/vue' route('index') route('users.show', { user: 1 }) ``` ::: :::info Reactivity The `route` function is not reactive. It returns a `string`, not a `Ref`. ::: ## Excluding or including routes By default, vendor routes are not made available to the front-end and will not appear when using the `route` util. This can be configured by updating the `router.allowed_vendors` key in `config/hybridly.php`. ```php return [ 'router' => [ 'allowed_vendors' => [ // [!code focus:3] 'laravel/fortify', ], 'exclude' => [], ], ]; ``` Additionally, you may exclude specific routes by adding patterns to the `router.exclude` key. These filters are matched against **the final URI**, and support wildcards. ```php return [ 'router' => [ 'allowed_vendors' => [], 'exclude' => [ // [!code focus:3] 'admin*' ], ], ]; ``` --- --- url: /guide/scroll-management.md --- # Scroll management ## Overview When navigating between pages, Hybridly simulates default browser behavior by automatically resettting the scroll position of the body back to the top. Additionally, the scroll position of each page is automatically restored when navigating back and forward in history. ## Preserving scroll position In some situations, it's desirable to prevent resetting the scroll position. This behavior can be disabled by setting the `preserveScroll` option to `true`. ```ts router.get(url, { preserveScroll: true }) ``` For instance, you may want to toggle a user setting after a click on a button by making a hybrid `POST` request on an endpoint and redirecting back. In that situation, it's not desirable to reset the scroll position. ## Scroll regions You may optionally define elements which scroll positions should be kept track of. This is done by applying the `scroll-region` attribute to an element. ```vue-html
``` The scroll position of these elements will be restored when navigating back and forward in the history. --- --- url: /guide/ssr.md --- # Server-side rendering ## On the roadmap Hybridly does not currently support server-side rendering, though we aim to provide first-class support for it in the future. The intent is to provide a single development server instead of both the Vite server and the SSR one. ## Is it really useful? While server-side rendering has benefits, it also has its share of drawbacks. Before looking into implementing it, you should ask yourself if your project really needs it. The main reason one would want to implement server-side rendering is to benefit from search engine optimization. However, Hybridly is primarily meant to power interaction-rich single-page applications - the kind of application that is not usually critically dependant on search engine optimization. If your application is not rich in interactions, maybe [Livewire](https://laravel-livewire.com) is a better fit for you. ## How indexing works It is worthy of noting that client-side-rendered applications are still indexed by Google. Hybridly applications without server-side rendering use the [application shell model](https://web.dev/learn/pwa/architecture/), where the HTML doesn't contain actual content and JavaScript needs to be executed. [Google does crawl such applications](https://developers.google.com/search/docs/crawling-indexing/javascript/javascript-seo-basics#how-googlebot-processes-javascript), but it puts them on a render queue. An application on Google's render queue can take a few seconds to a few days to be crawled and indexed, depending on Google's resources at the moment. If your application is not *critically* dependant on search engine optimization, you probably don't need server-side rendering. --- --- url: /guide/tables.md --- # Tables ## Overview Hybridly provides a way to describe tables on the back-end and manipulate them through the [`useTable`](../api/utils/use-table.md) util on the front-end. Tables provide the ability to execute actions on one or multiple records, to filter and sort them using [refinements](./refining.md), have [data objects](#using-data-objects) integration, support pagination, scoping, and let you have full control over the user interface. ## Creating and using tables ### Generating the table class A table is defined by a class that extends `Hybridly\Tables\Table`. The associated model is inferred by the class name, but can be specified by using the `$model` class property. ```php use App\Models\User; use Hybridly\Tables\Columns\TextColumn; use Hybridly\Tables\Table; final class UsersTable extends Table { protected string $model = User::class; protected function defineColumns(): array { return [ TextColumn::make('id')->label('#'), TextColumn::make('full_name')->transformValueUsing(fn (User $user) => "{$user->first_name} {$user->last_name}"), ]; } } ``` As a convenience, you may use the provided `make:table` command to generate a table: ``` php artisan make:table UsersTable ``` Optionally, you may provide the `--model` argument to specify the associated model. ### Accessing tables in views Like refinements, you may simply pass the table as a property to the hybrid view returned by a controller: ```php use function Hybridly\view; return view('users.index', [ 'users' => UsersTable::make(), ]); ``` The table property can be typed using the global `Table` type, which also accepts a generic that describes the table shape: ```ts const $props = defineProps<{ users: Table<{ id: number full_name: string }> }>() const users = useTable($props, 'users') ``` At its simplest, the template for a table may look like that: ```vue ``` :::tip User interface The user interface is completely up to you, no component is provided. You may refer to the [`useTable`](../api/utils/use-table.md) documentation to see which utilities are available to work with tables. ::: ### Multiple tables It is possible to work with multiple tables in the same view, but for filters and pagination to work, they need to be scoped. This can be done by specifying the `$scope` class property: :::code-group ```php [UsersTable.php] use App\Models\User; use Hybridly\Tables\Table; final class UsersTable extends Table { protected string $model = User::class; protected string $scope = 'users'; // ... } ``` ```php [ProjectsTable.php] use App\Models\Project; use Hybridly\Tables\Table; final class ProjectsTable extends Table { protected string $model = Project::class; protected string $scope = 'projects'; // ... } ``` ```php [Controller.php] use function Hytbridly\view; return view('dashboard', [ 'users' => UsersTable::make(), 'projects' => ProjectsTable::make(), ]); ``` ```ts [dashboard.vue] const $props = defineProps<{ users: Table projects: Table }>() const users = useTable($props, 'users') const projects = useTable($props, 'projects') ``` ::: When scoping tables, refining records and pagination will automatically work through the utilities provided by [`useTable`](../api/utils/use-table.md). ## Working with columns ### Defining columns Columns specify what record properties will be available on the front-end. They are defined inside the `defineColumns` method, using the `TextColumn` class: ```php use Hybridly\Tables\Columns\TextColumn; // [!code focus] protected function defineColumns(): array // [!code focus:8] { return [ TextColumn::make('id')->label('#'), TextColumn::make('full_name'), TextColumn::make('email'), ]; } ``` The name passed to the `make` constructor should refer to a valid model property. ### Specifying labels By default, the label is generated from the column name. You may customize it using the `label` method: ```php TextColumn::make('full_name') ->label('Name') ``` You may access the label in the `column` object: ```vue-html ``` ### Transforming column values It is often desirable to transform the value of a column. To achieve this, you may pass a callback to the `transformValueUsing` function: ```php TextColumn::make('full_name') ->transformValueUsing(function (User $user) { return "{$user->first_name} {$user->last_name}"; }) ``` The value of a column is accessible in the `records` property of the table: ```vue-html ``` ### Hiding columns You may dynamically hide columns by passing a boolean or callback to the `hidden` function: ```php TextColumn::make('id') ->hidden(fn () => ! auth()->user()->is_admin) ``` Hidden columns **are not transmitted to the front-end at all**, and their corresponding model properties will not be available. If you need to hide a column but still have access to its properties, you may use [metadata](#adding-metadata) instead. ### Adding metadata You may pass any information to a column by passing an array to the `metadata` function. Note that the metadata applies to the actual column object, not the properties of the records. :::code-group ```php [UsersTable.php] TextColumn::make('full_name')->metadata([ 'color' => 'primary' ]) ``` ```vue-html [index.vue] ``` ::: ## Refining records Filtering and sorting tables records works by leveraging the existing [refining](./refining.md) features. You can define or apply refiners using the same API on both the back-end and front-end. ### Defining refiners You may define the available filters and sorts for a table by specifying [refiners](./refining.md#specifying-refiners) in the `defineRefiners` method: ```php use Hybridly\Refining\{Filters, Sorts}; // [!code focus] protected function defineRefiners(): array // [!code focus:7] { return [ Sorts\Sort::make('id'), Filters\TextFilter::make('full_name'), ]; } ``` ### Applying refiners The refining utilities returned by [`useTable`](../api/utils/use-table.md) are the same as the ones returned by [`useRefinements`](../api/utils/use-refinements.md). For instance, you may generate the user interface for a [similarity filter](./refining.md#loose-comparisons) using the following: ```vue-html
``` ### Transforming the base query The base query is automatically generated from the underlying model. You may override the `defineQuery` method to customize it entirely: ```php use Illuminate\Contracts\Database\Eloquent\Builder; protected function defineQuery(): Builder { return $this->getModel()->query(); } ``` ## Working with actions Hybridly supports inline and bulk actions. Inline actions can be used to execute code for a specific record, while bulk-actions can execute code for multiple records at once. ### Defining actions Both inline and bulk actions are defined in the `defineActions` method: ```php use Hybridly\Tables\Actions\{InlineAction, BulkAction}; use Illuminate\Database\Eloquent\Collection; protected function defineActions(): array { return [ InlineAction::make('delete') ->action(fn (User $user) => $user->delete()), BulkAction::make('delete') ->action(fn (Collection $records) => $records->each->delete()), ]; } ``` The `action` method accepts a callback that will be executed when the action is called from the front-end. Dependencies from the container may be injected to that callback. ### Inline actions The callback for inline actions accepts the typed record as a parameter. When not specifying types, the parameter *must* be named `$record`. ```php InlineAction::make('delete')->action(fn (User $user) => $user->delete()) InlineAction::make('delete')->action(fn ($record) => $record->delete()) ``` ### Bulk actions The callback for bulk actions accepts a `Collection` parameter with any name, or a `$records` parameter if not typed. ```php BulkAction::make('delete') ->action(fn (Collection $records) => $records->each->delete()) ``` However, when selecting a lot of records, it may be inefficient to load them all in memory. For this reason, you may inject a `Builder` instance instead: ```php use Illuminate\Contracts\Database\Eloquent\Builder; BulkAction::make('delete') ->action(fn (Builder $query) => $query->delete()) ``` ### Selecting records The `useTable` function returns utilities to select records. The selected records are scoped to the `useTable` call, so all bulk actions will use them. To let users select records, you may use the `bindCheckbox` function. It takes the record key as the parameter and returns the necessary properties and event listeners to support all selection states. ```vue-html ``` ### Automatically de-selecting records By default, executing a bulk-action will de-select all records. You may change this behavior by calling the `keepSelected` method on an action: ```php BulkAction::make('archive') ->keepSelected() ->action(fn (Collection $records) => $records->each->archive()), ``` ### Hiding actions Like columns, actions may be hidden depending on a specific condition. ```php InlineAction::make('delete') ->action(fn (User $user) => $user->delete()) ->hidden(fn () => ! auth()->user()->is_admin) ``` Hidden actions are not sent to the front-end and cannot be executed, even when manually calling the action endpoint. ### Using actions Actions work by making a `POST` hybrid request to a dedicated endpoint. The [`useTable`](../api/utils/use-table.md) util returns dedicated functions to access and execute inline and bulk actions: :::code-group ```vue-html [bulk-actions.vue]
``` ```vue-html [inline-actions.vue]