{"id":17024310,"url":"https://github.com/littlesound/slimeform","last_synced_at":"2025-04-12T18:52:25.483Z","repository":{"id":37470388,"uuid":"492873540","full_name":"LittleSound/slimeform","owner":"LittleSound","description":"Form state management and validation for Vue3","archived":false,"fork":false,"pushed_at":"2024-11-05T06:57:56.000Z","size":477,"stargazers_count":267,"open_issues_count":3,"forks_count":8,"subscribers_count":4,"default_branch":"main","last_synced_at":"2025-04-12T10:57:20.126Z","etag":null,"topics":["form","utilities-library","vue3","vue3-typescript"],"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/LittleSound.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","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},"funding":{"github":"LittleSound"}},"created_at":"2022-05-16T14:34:58.000Z","updated_at":"2025-04-05T12:02:23.000Z","dependencies_parsed_at":"2024-02-12T06:29:16.376Z","dependency_job_id":"30884e92-43dc-4a00-88fb-04f94422061b","html_url":"https://github.com/LittleSound/slimeform","commit_stats":{"total_commits":75,"total_committers":6,"mean_commits":12.5,"dds":0.28,"last_synced_commit":"c143529b7ed12433c090b9d3613701457584c285"},"previous_names":[],"tags_count":19,"template":false,"template_full_name":"sachinraja/ts-lib-starter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LittleSound%2Fslimeform","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LittleSound%2Fslimeform/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LittleSound%2Fslimeform/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/LittleSound%2Fslimeform/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/LittleSound","download_url":"https://codeload.github.com/LittleSound/slimeform/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248618237,"owners_count":21134200,"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":["form","utilities-library","vue3","vue3-typescript"],"created_at":"2024-10-14T07:25:13.310Z","updated_at":"2025-04-12T18:52:25.449Z","avatar_url":"https://github.com/LittleSound.png","language":"TypeScript","readme":"\u003cbr\u003e\n\u003cbr\u003e\n\u003cbr\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg height=\"80px\" src=\"https://github.com/LittleSound/slimeform/raw/main/example/public/favicon.svg\"\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003eSlimeForm\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/sponsors/LittleSound\"\u003e\n    \u003cimg src=\"https://cdn.jsdelivr.net/gh/littlesound/sponsors/sponsors.svg\"/\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  This project is made possible by all the sponsors supporting my work \u003cbr\u003e\n  You can join them at my sponsors profile:\n\u003c/p\u003e\n\u003cp align=\"center\"\u003e\u003ca href=\"https://github.com/sponsors/LittleSound\"\u003e\u003cimg src=\"https://img.shields.io/static/v1?label=Sponsor\u0026message=%E2%9D%A4\u0026logo=GitHub\u0026color=%23fe8e86\u0026style=for-the-badge\" /\u003e\u003c/a\u003e\u003c/p\u003e\n\n---\n\n\u003cp align=\"center\"\u003eEnglish | \u003ca href=\"https://github.com/LittleSound/slimeform/blob/HEAD/README.zh-Hans.md\"\u003e简体中文\u003c/a\u003e\u003c/p\u003e\n\n\u003c!-- 一些美丽的标签 --\u003e\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://www.npmjs.com/package/slimeform\"\u003e\n    \u003cimg alt=\"npm\" src=\"https://badgen.net/npm/v/slimeform\"\u003e\n  \u003c/a\u003e\n  \u003c!-- \u003ca href=\"https://github.com/LittleSound/slimeform/actions/workflows/test.yaml\"\u003e\n    \u003cimg alt=\"Test\" src=\"https://github.com/LittleSound/slimeform/actions/workflows/test.yaml/badge.svg\"\u003e\n  \u003c/a\u003e --\u003e\n  \u003ca href=\"#try-it-online\"\u003e\n    \u003cimg alt=\"docs\" src=\"https://img.shields.io/badge/-docs%20%26%20demos-1e8a7a\"\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n\u003cbr\u003e\n\nForm state management and validation\n\n## Why?\n\nWe usually use all sorts of different pre-made form components in vue projects which may be written by ourselves, or come from other third-party UI libraries. As for those third-party UI libraries, they might shipped their own form validators with libraries, however we still will need to build our form validators for those components written by us. In most of the time, those form validators were not 'unified' or we say compatible to the others, especially when you mixed your own components with third-party components together in one project where thing might become tricky.\n\nBase on modern CSS utilities class and component-based design, it has now become way more easier to write your own `\u003cinput\u003e` component in specific style and assemble them as a form, however, when you need to integrate form state management and rule validation with all the related input fields, the problem will be more complex.\n\nSo I started to experiment a solution to achieve this kind of functionalities, and naming it with SlimeForm, which means this utilities would try it best to fit in the forms just like the slime does 💙.\n\nSlimeForm is a form state management and validator which is **dependency free**, **no internal validation rules shipped and required**. By binding all native or custom components through `v-model`, SlimeForm is able to manage and validate values reactively.\n\n## TODO\n\n- [x] Improve the functionalities\n  - [x] Use reactive type to return the form\n  - [x] For a single rule, the array can be omitted\n  - [x] Mark whether the value of the form has been modified\n- [x] Documentations\n- [x] Better type definitions for Typescript\n- [x] Unit tests\n- [x] Add support to fields with `object` type\n- [ ] Add support to async rule validation\n- [x] Support filtering the unmodified entries in the form, leaving only the modified entries for submission\n- [ ] Support for third-party rules, such as [yup](https://github.com/jquense/yup)\n  - [x] Support `validateSync`\n  - [ ] Support `validate` (Async)\n- [ ] 💡 More ideas...\n\n### Contributions are welcomed\n\n## Try it online\n\n🚀 [slimeform-playground](https://stackblitz.com/edit/vitejs-vite-4eppne?file=package.json,src%2FApp.vue,src%2Fcomponents%2FHeader.vue,src%2Fcomponents%2FDemo.vue\u0026terminal=dev)\n\n## Install\n\n\u003e ⚗️ **Experimental**\n\n```shell\nnpm i slimeform\n```\n\n\u003e SlimeForm only works with Vue 3\n\n## Usage\n\n### Form state management\n\nUse `v-model` to bind `form[key]` on to the `\u003cinput\u003e` element or other components.\n\n`status` value will be changed corresponded when the form values have been modified. Use the `reset` function to reset the form values back to its initial states.\n\n```vue\n\u003cscript setup\u003e\nimport { useForm } from 'slimeform'\n\nconst { form, status, reset, dirtyFields } = useForm({\n  // Initial form value\n  form: () =\u003e ({\n    username: '',\n    password: '',\n  }),\n})\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n  \u003cform @submit.prevent=\"mySubmit\"\u003e\n    \u003clabel\u003e\n      \u003c!-- here --\u003e\n      \u003cinput v-model=\"form.username\" type=\"text\"\u003e\n      \u003cinput v-model=\"form.password\" type=\"text\"\u003e\n    \u003c/label\u003e\n    \u003cbutton type=\"submit\"\u003e\n      Submit\n    \u003c/button\u003e\n  \u003c/form\u003e\n\u003c/template\u003e\n```\n\n#### State management\n\n```ts\nconst { form, status, reset, isDirty } = useForm(/* ... */)\n\n// whether the form has been modified\nisDirty.value\n// whether the username has been modified\nstatus.username.isDirty\n// whether the password has been modified\nstatus.password.isDirty\n\n// Reset form, restore form values to default\nreset()\n\n// Reset the specified fields\nreset('username', 'password', /* ... */)\n```\n\n### Mutable initial value of form\n\nThe initial states of `useForm` could be any other variables or pinia states. The changes made to the initial values will be synced into the `form` object when the form has been resetted.\n\n```ts\nconst userStore = useUserStore()\n\nconst { form, reset } = useForm({\n  form: () =\u003e ({\n    username: userStore.username,\n    intro: userStore.intro,\n  }),\n})\n\n// update the value of username and intro properties\nuserStore.setInfo(/* ... */)\n// changes made to the `userStore` will be synced into the `form` object,\n// when reset is being called\nreset()\n\n// these properties will be the values of `userStore` where `setInfo` has been called previously\nform.username\nform.intro\n```\n\n### Filtering out modified fields\n\nSuppose you are developing a form to edit existing data, where the user usually only modifies some of the fields, and then the front-end submits the modified fields to the back-end via\n[HTTP  PATCH](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH) to submit the user-modified part of the fields to the backend, and the backend will partially update based on which fields were submitted\n\nSuch a requirement can use the `dirtyFields` computed function, whose value is an object that only contains the modified fields in the `form`.\n\n```ts\nconst { form: userInfo, status, dirtyFields } = useForm(/* ... */)\n\ndirtyFields.value /* value: {} */\n\n// Edit user intro\nuserInfo.intro = 'abcd'\n\ndirtyFields.value /* value: { intro: 'abcd' } */\n\n// Edit user profile to default\nuserInfo.intro = '' /* default value */\n\ndirtyFields.value /* value: {} */\n```\n\n### Validating rules for form\n\nUse `rule` to define the validation rules for form fields. The verification process will be take placed automatically when values of fields have been changed, the validation result will be stored and provided in `status[key].isError` and `status[key].message` properties. If one fields requires more than one rule, it can be declared by using function arrays.\n\n\u003e You can also maintain your rule collections on your own, and import them where they are needed.\n\n```ts\n// formRules.ts\nfunction isRequired(value) {\n  if (value \u0026\u0026 value.trim())\n    return true\n\n  return t('required') // i18n support\n}\n```\n\n```vue\n\u003cscript setup\u003e\nimport { isRequired } from '~/util/formRules.ts'\nconst {\n  form,\n  status,\n  submitter,\n  clearErrors,\n  isError,\n  verify\n} = useForm({\n  // Initial form value\n  form: () =\u003e ({\n    name: '',\n    age: '',\n  }),\n  // Verification rules\n  rule: {\n    name: isRequired,\n    // If one fields requires more then one rule, it can be declared by using function arrays.\n    age: [\n      isRequired,\n      // is number\n      val =\u003e !Number.isNaN(val) || 'Expected number',\n      // max length\n      val =\u003e val.length \u003c 3 || 'Length needs to be less than 3',\n    ],\n  },\n})\n\nconst { submit } = submitter(() =\u003e {\n  alert(`Age: ${form.age} \\n Name: ${form.name}`)\n})\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n  \u003cform @submit.prevent=\"submit\"\u003e\n    \u003c!-- ... --\u003e\n  \u003c/form\u003e\n\u003c/template\u003e\n```\n\nIn addition, you can use any reactive values in the validation error message, such as the `t('required')` function call from `vue-i18n` as the examples shown above.\n\n\u003cdetails\u003e\u003csummary\u003eManually trigger the validation\u003c/summary\u003e\n\u003cp\u003e\n\n```ts\nconst { _, status, verify } = useForm(/* ... */)\n// validate the form\nverify()\n// validate individual fields\nstatus.username.verify()\n```\n\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eManually specify error message\u003c/summary\u003e\n\u003cp\u003e\n\n```ts\nstatus.username.setError('username has been registered')\n```\n\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eMaunally clear the errors\u003c/summary\u003e\n\u003cp\u003e\n\n```ts\nconst { _, status, clearErrors, reset } = useForm(/* ... */)\n// clear the error for individual field\nstatus.username.clearError()\n// clear all the errors\nclearErrors()\n// reset will also clear the errors\nreset()\n```\n\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eAny errors\u003c/summary\u003e\n\u003cp\u003e\n\n`isError`: Are there any form fields that contain incorrect validation results\n\n```ts\nconst { _, isError } = useForm(/* ... */)\n\nisError /* true / false */\n```\n\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eDefault message for form\u003c/summary\u003e\n\u003cp\u003e\n\nUse `defaultMessage` to define a placeholders for the form field validation error message. The default value is `''`, you can set it to `\\u00A0`, which will be escaped to `\u0026nbsp;` during rendering, to avoid the height collapse problem of `\u003cp\u003e` when there is no messages.\n\n```ts\nconst { form, status } = useForm({\n  form: () =\u003e ({/* ... */}),\n  rule: {/* ... */},\n  // Placeholder content when there are no error message\n  defaultMessage: '\\u00A0',\n})\n```\n\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eLazy rule validation\u003c/summary\u003e\n\u003cp\u003e\n\nYou can set `lazy` to `true` to prevent rules from being automatically verified when data changes.\n\nIn this case, consider call `verify()` or `status[fieldName].verify()` to manually validate fields.\n\n```ts\nconst { form, status, verify } = useForm({\n  form: () =\u003e ({\n    userName: '',\n    /* ... */\n  }),\n\n  rule: {\n    userName: v =\u003e v.length \u003c 3,\n  },\n\n  lazy: true,\n})\n\nform.userName = 'abc'\nstatus.userName.isError // false\n\nverify()\n\nstatus.userName.isError // true\n```\n\n\u003c/p\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003e\u003ccode\u003erule\u003c/code\u003e in return value of \u003ccode\u003euseForm()\u003c/code\u003e\u003c/summary\u003e\n\u003cp\u003e\n\nSlimeform provides `rule` in return value of `useForm()`, which can be used to validate data not included in form. This can be useful if you want to make sure anything passing into form is valid.\n\n```ts\nconst { form, rule } = useForm({\n  form: () =\u003e ({\n    userName: '',\n    /* ... */\n  }),\n\n  rule: {\n    userName: v =\u003e v.length \u003c 3 || 'to many characters',\n  },\n})\n\nconst text = 'abcd'\nconst isValid = rule.userName.validate(text) // false\nif (isValid)\n  form.userName = text\n```\n\nYou can also get access to the error message by indicating `fullResult: true` in the second options argument, in which case an object containing the message will be returned.\n\n```ts\nrule.userName.validate('abcd', { fullResult: true }) // { valid: false, message: \"to many characters\" }\nrule.userName.validate('abc', { fullResult: true }) // { valid: true, message: null }\n```\n\n\u003c/p\u003e\n\u003c/details\u003e\n\n### Submission\n\n`submitter` accepts a callback function as argument which returns the function that be able to triggered this callback function and a state variable that indicates the function is running. The callback function passed into `submitter` can get all the states and functions returned by the `useForm`, which allows you to put the callback function into separate code or even write generic submission functions for combination easily.\n\n```vue\n\u003cscript setup\u003e\nimport { useForm } from 'slimeform'\n\nconst { _, submitter } = useForm(/* ... */)\n\n// Define the submit function\nconst {\n  // trigger submit callback\n  submit,\n  // Indicates whether the asynchronous commit function is executing\n  submitting,\n} = submitter(async ({ form, status, isError, reset /* ... */ }) =\u003e {\n  // Submission Code\n  const res = await fetch(/* ... */)\n  // ....\n})\n\u003c/script\u003e\n\n\u003ctemplate\u003e\n  \u003cform @submit.prevent=\"submit\"\u003e\n    \u003c!-- ... --\u003e\n\n    \u003c!-- Use `submitting` to disable buttons and add loading indicator --\u003e\n    \u003cbutton type=\"submit\" :disabled=\"submitting\"\u003e\n      \u003cicon v-if=\"submitting\" name=\"loading\" /\u003e\n      Submit\n    \u003c/button\u003e\n  \u003c/form\u003e\n\u003c/template\u003e\n```\n\nBy default, the form rules validation will take place first after the `submit` function have been called, if the validation failed, the function call will be terminated immediately.\nIf you want to turn off this behavior, you can configure `enableVerify: false` option in the second parameter `options` of the `submitter` to skip the validation.\n\n#### Wrap the generic submission code and use it later\n\n```ts\nimport { mySubmitForm } from './myFetch.ts'\nconst { _, submitter } = useForm(/* ... */)\n// Wrap the generic submission code and use it later\nconst { submit, submitting } = submitter(mySubmitForm({ url: '/register', method: 'POST' }))\n```\n\n## Integrations\n\n### Using Yup as a rule\n\nIf you don't want to write the details of validation rules yourself, there is already a very clean way to use [Yup](https://github.com/jquense/yup) as a rule.\n\nSlimeForm has a built-in resolvers for [Yup](https://github.com/jquense/yup) synchronization rules: `yupFieldRule`, which you can import from `slimeform/resolvers`. `yupFieldRule` function internally calls `schema.validateSync` method and processes the result in a format acceptable to SlimeForm.\n\n**First, you have to install [Yup](https://github.com/jquense/yup)**\n\n```sh\nnpm install yup\n```\n\nthen import `yup` and `yupFieldRule` into your code and you're ready to go!\n\n```ts\nimport { useForm } from 'slimeform'\nimport * as yup from 'yup'\n\n/* Importing a resolvers */\nimport { yupFieldRule } from 'slimeform/resolvers'\n\nconst { t } = useI18n()\n\nconst { form, status } = useForm({\n  form: () =\u003e ({ age: '' }),\n  rule: {\n    /* Some use cases */\n    age: [\n      yupFieldRule(yup.string()\n        .required(),\n      ),\n      yupFieldRule(yup.number()\n        .max(120, () =\u003e t('xxx_i18n_key'))\n        .integer()\n        .nullable(),\n      ),\n    ],\n  },\n})\n```\n\n## Suggestions\n\nSome suggestions:\n\n1. Use `@submit.prevent` instead of `@submit`, this can prevent the submitting action take place by form's default\n2. Use `isError` to determine whether to add a red border around the form dynamically\n\n```vue\n\u003ctemplate\u003e\n  \u003ch3\u003ePlease enter your age\u003c/h3\u003e\n  \u003cform @submit.prevent=\"submitFn\"\u003e\n    \u003clabel\u003e\n      \u003cinput\n        v-model=\"form.age\"\n        type=\"text\"\n        :class=\"status.age.isError \u0026\u0026 '!border-red'\"\n      \u003e\n      \u003cp\u003e{{ status.age.message }}\u003c/p\u003e\n    \u003c/label\u003e\n    \u003cbutton type=\"submit\"\u003e\n      Submit\n    \u003c/button\u003e\n  \u003c/form\u003e\n\u003c/template\u003e\n```\n","funding_links":["https://github.com/sponsors/LittleSound"],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flittlesound%2Fslimeform","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flittlesound%2Fslimeform","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flittlesound%2Fslimeform/lists"}