{"id":25018093,"url":"https://github.com/ericvera/vue-bare-composables","last_synced_at":"2026-02-19T05:03:45.115Z","repository":{"id":274877646,"uuid":"924353826","full_name":"ericvera/vue-bare-composables","owner":"ericvera","description":"Vue composables for a frustration-free development experience","archived":false,"fork":false,"pushed_at":"2025-04-07T06:16:07.000Z","size":1597,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-07T07:25:17.271Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ericvera.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-01-29T21:09:10.000Z","updated_at":"2025-04-07T06:16:04.000Z","dependencies_parsed_at":"2025-02-17T07:20:36.706Z","dependency_job_id":"6afa8eb0-ab8f-4ac2-89ac-feda50fb15f7","html_url":"https://github.com/ericvera/vue-bare-composables","commit_stats":null,"previous_names":["ericvera/vue-bare-composables"],"tags_count":12,"template":false,"template_full_name":"ericvera/ts-lib-template","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericvera%2Fvue-bare-composables","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericvera%2Fvue-bare-composables/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericvera%2Fvue-bare-composables/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ericvera%2Fvue-bare-composables/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ericvera","download_url":"https://codeload.github.com/ericvera/vue-bare-composables/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248654030,"owners_count":21140235,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-02-05T10:27:44.049Z","updated_at":"2026-02-19T05:03:45.108Z","avatar_url":"https://github.com/ericvera.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Vue Bare Composables\n\n**Vue composables for a frustration-free development experience**\n\n[![github license](https://img.shields.io/github/license/ericvera/vue-bare-composables.svg?style=flat-square)](https://github.com/ericvera/vue-bare-composables/blob/main/LICENSE)\n[![npm version](https://img.shields.io/npm/v/vue-bare-composables.svg?style=flat-square)](https://npmjs.org/package/vue-bare-composables)\n\n## Features\n\n- 🎯 **Type-safe**: Built with TypeScript using the strictest configuration\n- 🪶 **Lightweight**: Zero dependencies besides Vue (Pinia required for useSnackbarStore)\n- 🧩 **Modular**: Use only what you need\n- 📦 **Tree-shakeable**: Unused code is removed in production builds\n- 🔍 **Form validation**: Built-in support for field and form-level validation\n\n## Installation\n\n```bash\n# npm\nnpm install vue-bare-composables\n\n# yarn\nyarn add vue-bare-composables\n\n# pnpm\npnpm add vue-bare-composables\n```\n\n## Requirements\n\n- Vue 3.x or higher\n- Node.js 22 or higher\n\n## Usage\n\nAvailable composables:\n\n- [useForm (Form Handling)](#useform-form-handling)\n- [useFixToVisualViewport (Visual Viewport Fixed Positioning)](#usefixtovisualviewport-visual-viewport-fixed-positioning)\n- [useIsWindowFocused (Window Focus Detection)](#useiswindowfocused-window-focus-detection)\n- [useSnackbarStore (Toast/Snackbar Notifications)](#usesnackbarstore-toastsnackbar-notifications)\n\n### useForm (Form Handling)\n\n```ts\nimport { useForm } from 'vue-bare-composables'\n\n// In your Vue component\nconst { state, getProps, getListeners, handleSubmit, reset } = useForm(\n  {\n    email: '',\n    password: '',\n  },\n  {\n    validate: {\n      email: (value) =\u003e {\n        if (!value) {\n          return 'Email is required'\n        }\n\n        if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(value as string)) {\n          return 'Invalid email format'\n        }\n      },\n      password: (value) =\u003e {\n        if (!value) {\n          return 'Password is required'\n        }\n\n        if ((value as string).length \u003c 8) {\n          return 'Password must be at least 8 characters'\n        }\n      },\n    },\n    globalValidate: async (values, { setError, setGlobalError }) =\u003e {\n      // Example of form-level validation\n      if (values.password === '12345678') {\n        setError('password', 'Password is too common')\n      }\n    },\n    // Automatically trim string values before validation and submission\n    trimStrings: true,\n  },\n)\n\n// Use in template\nconst onSubmit = handleSubmit(async (data) =\u003e {\n  // Handle form submission\n  console.log(data)\n})\n```\n\nIn your template:\n\n```vue\n\u003ctemplate\u003e\n  \u003cform @submit.prevent=\"onSubmit\"\u003e\n    \u003cspan v-if=\"state.globalError.value\"\u003e{{ state.globalError.value }}\u003c/span\u003e\n\n    \u003cinput\n      id=\"email\"\n      type=\"email\"\n      v-bind=\"getProps('email')\"\n      v-on=\"getListeners('email')\"\n    /\u003e\n    \u003cspan v-if=\"state.errors.email.value\"\u003e\n      {{ state.errors.email.value }}\n    \u003c/span\u003e\n\n    \u003cinput\n      id=\"password\"\n      type=\"password\"\n      v-bind=\"getProps('password')\"\n      v-on=\"getListeners('password')\"\n    /\u003e\n    \u003cspan v-if=\"state.errors.password.value\"\u003e\n      {{ state.errors.password.value }}\n    \u003c/span\u003e\n\n    \u003cbutton type=\"submit\" :disabled=\"state.submitting.value\"\u003eSubmit\u003c/button\u003e\n    \u003cbutton type=\"button\" :disabled=\"!state.isDirty.value\" @click=\"reset()\"\u003e\n      Reset\n    \u003c/button\u003e\n  \u003c/form\u003e\n\u003c/template\u003e\n```\n\nThe `useForm` composable provides the following features:\n\n- **Type-safe form handling**: Full TypeScript support with strict type checking\n- **Field-level validation**: Validate individual fields with custom validation functions\n- **Form-level validation**: Validate multiple fields together or perform cross-field validation\n- **Automatic error handling**: Display validation errors and manage error states\n- **Form submission handling**: Handle form submissions with loading states\n- **String trimming**: Optionally trim string values before validation and submission\n- **Form reset**: Reset form to initial values\n- **Dirty state tracking**: `state.isDirty` indicates whether form values differ from initial values (useful for unsaved-changes warnings, enabling reset buttons, etc.)\n- **Form-level errors**: `state.globalError` displays validation errors from `globalValidate` (e.g., cross-field validation)\n- **Reactive state**: All form state is reactive and can be watched for changes\n\n### Options\n\nThe `useForm` composable accepts the following options:\n\n- `validate`: Object containing field-level validation functions\n- `globalValidate`: Function for form-level validation\n- `trimStrings`: Boolean to enable automatic trimming of string values before validation and submission (defaults to false)\n- `trimStringExclude`: Array of field names to exclude from string trimming (only available when `trimStrings` is `true`)\n\n### Example with String Trimming\n\n```ts\nconst { state, handleSubmit } = useForm(\n  {\n    name: '',\n    email: '',\n    password: '',\n  },\n  {\n    trimStrings: true, // Enable automatic string trimming\n    trimStringExclude: ['password'], // Exclude password from trimming\n    validate: {\n      name: (value) =\u003e {\n        // Value will be automatically trimmed before validation\n        if (!value) {\n          return 'Name is required'\n        }\n\n        if (value.length \u003c 3) {\n          return 'Name must be at least 3 characters'\n        }\n      },\n      password: (value) =\u003e {\n        // Value will NOT be trimmed because it's in trimStringExclude\n        if (!value) {\n          return 'Password is required'\n        }\n\n        if (value.length \u003c 8) {\n          return 'Password must be at least 8 characters'\n        }\n      },\n    },\n  },\n)\n\n// When submitting, string values will be automatically trimmed except for excluded fields\nconst onSubmit = handleSubmit(async (data) =\u003e {\n  // data.name and data.email will be trimmed, but data.password won't\n  // The form's internal state will also be updated with the trimmed values\n  console.log(data)\n})\n```\n\n\u003e **Note**: The `trimStringExclude` option is only available when `trimStrings` is set to `true`. TypeScript will enforce this constraint at compile time.\n\u003e\n\u003e When string trimming is enabled, the form's internal state will be automatically updated with the trimmed values after a successful submission. This ensures that the form's state always matches what was actually submitted.\n\n### useFixToVisualViewport (Visual Viewport Fixed Positioning)\n\n```ts\nimport { useFixToVisualViewport } from 'vue-bare-composables'\n\n// In your Vue component\nconst element = ref\u003cHTMLElement | null\u003e(null)\n\n// For bottom positioning\nuseFixToVisualViewport(element, {\n  layoutViewportId: 'viewport',\n  location: 'bottom',\n})\n\n// For top positioning\nuseFixToVisualViewport(element, {\n  layoutViewportId: 'viewport',\n  location: 'top',\n})\n\n// For positioning above another element (pass the element, e.g. anotherElement.value if using a ref)\nuseFixToVisualViewport(element, {\n  layoutViewportId: 'viewport',\n  location: 'above',\n  relativeElement: anotherElement.value,\n  distance: 10,\n})\n\n// With reactive options (options that can change over time)\nconst viewportOptions = ref({\n  layoutViewportId: 'viewport',\n  location: 'bottom',\n})\n\nuseFixToVisualViewport(element, viewportOptions)\n\n// The position will update reactively when options change\nviewportOptions.value.location = 'top'\n```\n\nIn your template:\n\n```vue\n\u003ctemplate\u003e\n  \u003c!-- Your content --\u003e\n  \u003cdiv ref=\"element\"\u003e\n    This element will maintain its position relative to the visual viewport\n  \u003c/div\u003e\n\n  \u003c!-- This should be in the main page body --\u003e\n  \u003cdiv id=\"viewport\" /\u003e\n\u003c/template\u003e\n```\n\n### useIsWindowFocused (Window Focus Detection)\n\n```ts\nimport { useIsWindowFocused } from 'vue-bare-composables'\n\n// In your Vue component\nconst isFocused = useIsWindowFocused()\n```\n\nIn your template:\n\n```vue\n\u003ctemplate\u003e\n  \u003cdiv\u003e\n    Window is currently {{ isFocused.value ? 'focused' : 'not focused' }}\n  \u003c/div\u003e\n\u003c/template\u003e\n```\n\n### useSnackbarStore (Toast/Snackbar Notifications)\n\nA Pinia store for managing toast/snackbar notifications with support for:\n\n- Message queueing\n- Route-specific messages\n- Custom actions\n- Auto-dismissal\n- Manual dismissal\n\n```ts\nimport { useSnackbarStore } from 'vue-bare-composables'\n\n// In your Vue component\nconst snackbar = useSnackbarStore()\n\n// If used within Nuxt, you need to pass the Pinia store instance\n// const pinia = usePinia()\n// const snackbarStore = useSnackbarStore(pinia as Pinia)\n\n// Simple message\nsnackbar.enqueueMessage({ message: 'Operation successful!' })\n\n// Message with custom actions\nsnackbar.enqueueMessage({\n  message: 'Item deleted',\n  actions: [\n    {\n      text: 'Undo',\n      callback: () =\u003e {\n        // Handle undo action\n      },\n    },\n  ],\n})\n\n// Route-specific message (only shows on specified route)\nsnackbar.enqueueMessage({\n  message: 'Welcome to the dashboard',\n  route: '/dashboard',\n})\n\n// Set the current route whenever the route changes\nsnackbar.setRouteFullPath('/dashboard')\n```\n\nIn your template:\n\n```vue\n\u003ctemplate\u003e\n  \u003cdiv v-if=\"snackbar.message\" class=\"snackbar\"\u003e\n    {{ snackbar.message }}\n\n    \u003cdiv v-if=\"snackbar.actions\" class=\"actions\"\u003e\n      \u003cbutton\n        v-for=\"action in snackbar.actions\"\n        :key=\"action.text\"\n        @click=\"action.callback\"\n      \u003e\n        {{ action.text }}\n      \u003c/button\u003e\n    \u003c/div\u003e\n  \u003c/div\u003e\n\u003c/template\u003e\n```\n\nKey features:\n\n- Messages are queued and shown in order\n- Messages auto-dismiss after 8 seconds\n- Messages can be manually dismissed\n- Route-specific messages only show on matching routes\n- Custom actions with callbacks\n- Messages older than 30 seconds are automatically cleaned up\n- Reactive state management with Pinia\n\n## License\n\n[MIT](./LICENSE)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fericvera%2Fvue-bare-composables","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fericvera%2Fvue-bare-composables","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fericvera%2Fvue-bare-composables/lists"}