├── .gitattributes ├── .gitignore ├── example ├── index.js ├── package.json ├── router.js ├── index.html ├── App.vue └── examples │ ├── Composition.vue │ └── Simple.vue ├── .editorconfig ├── src ├── index.js ├── useCurrentForm.js ├── utils.js ├── useForm.js ├── Field.js ├── useField.js └── Form.js ├── vite.config.js ├── circle.yml ├── LICENSE ├── package.json ├── index.html └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/ 3 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | 5 | const app = createApp(App) 6 | 7 | app.use(router) 8 | 9 | app.mount('#app') 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import FinalForm from './Form.js' 2 | import FinalField from './Field.js' 3 | import useForm from './useForm.js' 4 | import useField from './useField.js' 5 | 6 | export { 7 | FinalForm, 8 | FinalField, 9 | useForm, 10 | useField, 11 | } 12 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "final-form": "^4.0.0", 8 | "vue": "^3.0.0", 9 | "vue-router": "^4.0.0", 10 | "vue-final-form": "latest" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/router.js: -------------------------------------------------------------------------------- 1 | import { h } from 'vue' 2 | import { createRouter, createWebHistory } from 'vue-router' 3 | import App from './App.vue' 4 | 5 | export default createRouter({ 6 | history: createWebHistory(), 7 | routes: [{ 8 | path: '/', 9 | component: { 10 | name: 'Home', 11 | render: () => h(App) 12 | } 13 | }] 14 | }) 15 | -------------------------------------------------------------------------------- /src/useCurrentForm.js: -------------------------------------------------------------------------------- 1 | import { inject } from 'vue' 2 | 3 | const useCurrentForm = () => { 4 | const finalForm = inject('finalForm') 5 | 6 | if (!finalForm) { 7 | throw new Error('Form was\'t found. Please provide it using `` component or `useForm` hook.') 8 | } 9 | 10 | return finalForm 11 | } 12 | 13 | export default useCurrentForm 14 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import { defineConfig } from 'vite' 3 | import vue from '@vitejs/plugin-vue' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | root: 'example', 8 | resolve: { 9 | alias: { 10 | 'vue-final-form': path.resolve('./src/index.js'), 11 | }, 12 | }, 13 | plugins: [vue()], 14 | }) 15 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue-Final-Form 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/repo 5 | docker: 6 | - image: circleci/node:latest 7 | branches: 8 | ignore: 9 | - gh-pages # list of branches to ignore 10 | - /release\/.*/ # or ignore regexes 11 | steps: 12 | - checkout 13 | - restore_cache: 14 | key: dependency-cache-{{ checksum "yarn.lock" }} 15 | - run: 16 | name: install dependences 17 | command: yarn 18 | - save_cache: 19 | key: dependency-cache-{{ checksum "yarn.lock" }} 20 | paths: 21 | - ./node_modules 22 | - run: 23 | name: test 24 | command: yarn test 25 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export const getChildren = children => 2 | Array.isArray(children) ? children : [children] 3 | 4 | const composeValidators = (validators, ...args) => 5 | // eslint-disable-next-line unicorn/no-array-reduce 6 | validators.reduce((error, validator) => error || validator(...args), undefined) 7 | 8 | export const composeFormValidators = validators => (...args) => 9 | composeValidators(validators, ...args) 10 | 11 | export const composeFieldValidators = validators => () => (...args) => 12 | composeValidators(validators, ...args) 13 | 14 | export const makeSubscriptionObject = subscriptionItems => 15 | // eslint-disable-next-line no-use-extend-native/no-use-extend-native 16 | Object.fromEntries(subscriptionItems.map(key => [key, true])) 17 | -------------------------------------------------------------------------------- /src/useForm.js: -------------------------------------------------------------------------------- 1 | import { provide, ref, onUnmounted } from 'vue' 2 | import { createForm, formSubscriptionItems } from 'final-form' 3 | import { composeFormValidators, makeSubscriptionObject } from './utils.js' 4 | 5 | const defaultSubscription = makeSubscriptionObject(formSubscriptionItems) 6 | 7 | const useForm = config => { 8 | const formApi = createForm({ 9 | initialValues: config.initialValues, 10 | validate: Array.isArray(config.validate) ? composeFormValidators(config.validate) : config.validate, 11 | onSubmit: config.onSubmit, 12 | }) 13 | 14 | const finalForm = ref({ 15 | ...formApi, 16 | handleSubmit: event => { 17 | event && event.preventDefault() 18 | formApi.submit() 19 | }, 20 | }) 21 | 22 | const formState = ref() 23 | 24 | const unsubscribe = finalForm.value.subscribe(state => { 25 | formState.value = state 26 | }, config.subscription || defaultSubscription) 27 | 28 | onUnmounted(unsubscribe) 29 | 30 | provide('finalForm', finalForm) 31 | 32 | return { 33 | finalForm, 34 | formState, 35 | unsubscribe, 36 | } 37 | } 38 | 39 | export default useForm 40 | -------------------------------------------------------------------------------- /src/Field.js: -------------------------------------------------------------------------------- 1 | import { getChildren } from './utils.js' 2 | import useField from './useField.js' 3 | 4 | const FinalField = { 5 | name: 'final-field', 6 | 7 | props: { 8 | name: { 9 | required: true, 10 | type: String, 11 | }, 12 | validate: [Function, Array], 13 | subscription: Object, 14 | }, 15 | 16 | setup(props) { 17 | const { finalForm, fieldState } = useField({ 18 | name: props.name, 19 | subscription: props.subscription, 20 | validate: props.validate, 21 | }) 22 | 23 | return { 24 | finalForm, 25 | fieldState, 26 | } 27 | }, 28 | 29 | watch: { 30 | fieldState(state) { 31 | this.$emit('change', state) 32 | }, 33 | }, 34 | 35 | render() { 36 | const { 37 | blur, 38 | change, 39 | focus, 40 | value, 41 | name, 42 | ...meta 43 | } = this.fieldState 44 | 45 | const children = this.$slots.default({ 46 | events: this.fieldState.events, 47 | change, 48 | value, 49 | name, 50 | meta, 51 | }) 52 | 53 | return getChildren(children)[0] 54 | }, 55 | } 56 | 57 | export default FinalField 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) EGOIST <0x142857@gmail.com> (https://egoist.moe) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/useField.js: -------------------------------------------------------------------------------- 1 | import { fieldSubscriptionItems } from 'final-form' 2 | import { ref, onUnmounted, computed } from 'vue' 3 | import useCurrentForm from './useCurrentForm.js' 4 | import { composeFieldValidators, makeSubscriptionObject } from './utils.js' 5 | 6 | const defaultSubscription = makeSubscriptionObject(fieldSubscriptionItems) 7 | 8 | const useField = config => { 9 | const finalForm = config.finalForm || useCurrentForm() 10 | 11 | const fieldState = ref({}) 12 | 13 | const unregister = finalForm.value.registerField(config.name, state => { 14 | fieldState.value = state 15 | }, config.subscription || defaultSubscription, { 16 | getValidator: Array.isArray(config.validate) ? composeFieldValidators(config.validate) : () => config.validate, 17 | }) 18 | 19 | onUnmounted(unregister) 20 | 21 | return { 22 | fieldState: computed(() => ({ 23 | ...fieldState.value, 24 | events: { 25 | input: event => fieldState.value.change(event.target.value), 26 | focus: () => fieldState.value.focus(), 27 | blur: () => fieldState.value.blur(), 28 | }, 29 | })), 30 | unregister, 31 | } 32 | } 33 | 34 | export default useField 35 | -------------------------------------------------------------------------------- /example/App.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 53 | 54 | 59 | 60 | 65 | -------------------------------------------------------------------------------- /src/Form.js: -------------------------------------------------------------------------------- 1 | import { h } from 'vue' 2 | import { getChildren } from './utils.js' 3 | import useForm from './useForm.js' 4 | 5 | const FinalForm = { 6 | name: 'final-form', 7 | 8 | props: { 9 | initialValues: Object, 10 | submit: { 11 | type: Function, 12 | default: () => {}, 13 | }, 14 | subscription: Object, 15 | validate: [Function, Array], 16 | }, 17 | 18 | setup(props) { 19 | const { finalForm, formState } = useForm({ 20 | initialValues: props.initialValues, 21 | validate: props.validate, 22 | subscription: props.subscription, 23 | onSubmit: props.submit, 24 | }) 25 | 26 | return { 27 | finalForm, 28 | formState, 29 | } 30 | }, 31 | 32 | methods: { 33 | handleSubmit(event) { 34 | event && event.preventDefault() 35 | this.finalForm.submit() 36 | }, 37 | }, 38 | 39 | watch: { 40 | formState(state) { 41 | this.$emit('change', state) 42 | }, 43 | }, 44 | 45 | render() { 46 | const children = this.$slots.default 47 | ? this.$slots.default({ 48 | ...this.formState, 49 | ...this.finalForm, 50 | }) 51 | : this.$slots.default 52 | 53 | return h('div', null, getChildren(children)) 54 | }, 55 | } 56 | 57 | export default FinalForm 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-final-form", 3 | "version": "4.0.1", 4 | "description": "Subscription-based form state management for Vue.js", 5 | "repository": { 6 | "url": "egoist/vue-final-form", 7 | "type": "git" 8 | }, 9 | "main": "dist/index.js", 10 | "module": "dist/index.esm.js", 11 | "unpkg": "dist/index.umd.js", 12 | "jsdelivr": "dist/index.umd.js", 13 | "files": [ 14 | "dist" 15 | ], 16 | "scripts": { 17 | "test": "npm run lint && echo 'no tests!'", 18 | "lint": "xo", 19 | "prepublishOnly": "npm run build", 20 | "build": "bili --format esm,cjs,umd,umd-min --module-name VueFinalForm --global.final-form final-form --global.vue vue", 21 | "example": "vite", 22 | "build:example": "vite build", 23 | "gh": "gh-pages -d example/dist", 24 | "deploy": "npm run build:example && npm run gh" 25 | }, 26 | "author": "egoist <0x142857@gmail.com>", 27 | "license": "MIT", 28 | "devDependencies": { 29 | "@vitejs/plugin-vue": "^1.10.0", 30 | "bili": "^5.0.5", 31 | "eslint-config-rem": "^4.0.0", 32 | "final-form": "^4.0.0", 33 | "gh-pages": "^1.0.0", 34 | "vite": "^2.0.0", 35 | "vue": "^3.0.0", 36 | "vue-router": "^4.0.0", 37 | "xo": "^0.47.0" 38 | }, 39 | "xo": { 40 | "extends": "rem", 41 | "envs": [ 42 | "browser" 43 | ], 44 | "ignores": [ 45 | "example/**" 46 | ], 47 | "rules": { 48 | "unicorn/filename-case": 0, 49 | "unicorn/prefer-export-from": 0, 50 | "import/prefer-default-export": 0 51 | } 52 | }, 53 | "peerDependencies": { 54 | "final-form": "^4.0.0" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vue-Final-Form 8 | 9 | 10 | 11 | 12 |
13 | 14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /example/examples/Composition.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 109 | -------------------------------------------------------------------------------- /example/examples/Simple.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-final-form 2 | 3 | 4 | 5 | [![NPM version](https://img.shields.io/npm/v/vue-final-form.svg?style=flat)](https://npmjs.com/package/vue-final-form) [![NPM downloads](https://img.shields.io/npm/dm/vue-final-form.svg?style=flat)](https://npmjs.com/package/vue-final-form) [![CircleCI](https://circleci.com/gh/egoist/vue-final-form/tree/master.svg?style=shield)](https://circleci.com/gh/egoist/vue-final-form/tree/master) [![donate](https://img.shields.io/badge/$-donate-ff69b4.svg?maxAge=2592000&style=flat)](https://github.com/egoist/donate) [![chat](https://img.shields.io/badge/chat-on%20discord-7289DA.svg?style=flat)](https://chat.egoist.moe) 6 | 7 | 8 | 9 | ## Introduction 10 | 11 | 🏁 High performance subscription-based form state management for Vue.js. 12 | 13 | ## Install 14 | 15 | ```bash 16 | yarn add final-form vue-final-form 17 | ``` 18 | 19 | ## Usage 20 | 21 | [![Edit example](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/egoist/vue-final-form/tree/master/example) 22 | 23 | This library exports two components: 24 | 25 | ```js 26 | import { FinalForm, FinalField } from "vue-final-form"; 27 | ``` 28 | 29 | The first component you'll need is the `FinalForm` component: 30 | 31 | ```vue 32 | 33 | 34 | 35 | ``` 36 | 37 | The only required prop is `submit`, it defines how to submit the form data, maybe simply log it: 38 | 39 | ```js 40 | function submit(values) { 41 | console.log(values); 42 | } 43 | ``` 44 | 45 | The rendered output is: 46 | 47 | ```html 48 |
49 | ``` 50 | 51 | As you can see it does nothing for now, you need to feed it a `
`: 52 | 53 | ```vue 54 | 55 | 56 | 57 | 58 |
59 | ``` 60 | 61 | Now it renders: 62 | 63 | ```html 64 |
65 | ``` 66 | 67 | Here it uses the [`scoped slots`](https://vuejs.org/v2/guide/components.html#Scoped-Slots) feature from Vue.js (>=2.1.0), `props.handleSubmit` is the actual method it will run to submit data. 68 | 69 | Next let's define a form field with `` component: 70 | 71 | ```vue 72 | 73 |
74 | 77 |
78 | 79 | 80 | {{ props.meta.error }} 81 | 82 |
83 |
84 |
85 |
86 | ``` 87 | 88 | Things got a bit more complex, but it's easy if you try to understand: 89 | 90 | The only prop that is required by `FinalField` is `name`, it tells the field where to store the field data in the form state, here we stores it as `state.username`. 91 | 92 | The `validate` prop is used to validate the field data, it could be a function that returns an error message or `null`, `undefined` when it's considered valid. 93 | 94 | The data that `FinalField` passed to its children contains `props.events` which is required to be bound with the `input` element in order to properly track events. And `props.name` is the `name` you gave `FinalField`, `props.meta` is some infomation about this field. 95 | 96 | ## API 97 | 98 | ### `` 99 | 100 | #### Props 101 | 102 | ##### submit 103 | 104 | Type: `function`
105 | Default: `() => {}` 106 | 107 | Here we allow `submit` to be optional since you may not need it when you just use `vue-final-form` as a form validation tool. 108 | 109 | See [onSubmit](https://github.com/final-form/final-form#onsubmit-values-object-form-formapi-callback-errors-object--void--object--promiseobject--void). 110 | 111 | ##### validate 112 | 113 | Type: `function` `Array` 114 | 115 | A whole-record validation function that takes all the values of the form and returns any validation errors. 116 | 117 | See [validate](https://github.com/final-form/final-form#validate-values-object--object--promiseobject). 118 | 119 | ##### initialValues 120 | 121 | Type: `object` 122 | 123 | See [initialValues](https://github.com/final-form/final-form#initialvalues-object). 124 | 125 | ##### subscription 126 | 127 | Type: `FormSubscription`
128 | Default: All 129 | 130 | See [FormSubscription](https://github.com/final-form/final-form#formsubscription--string-boolean-). 131 | 132 | #### Events 133 | 134 | ##### change 135 | 136 | Params: 137 | 138 | - `formState`: https://github.com/final-form/final-form#formstate 139 | 140 | #### Scoped slot props 141 | 142 | It basically exposes everything in [FormState](https://github.com/final-form/final-form#formstate) plus follwoings: 143 | 144 | ##### handleSubmit 145 | 146 | Type: `function` 147 | 148 | The function that you will invoke to submit the form data, you may use it as the `:submit` event handler on your `
`. 149 | 150 | ##### reset 151 | 152 | Type: `function` 153 | 154 | See [FormApi.reset](https://github.com/final-form/final-form#reset---void). 155 | 156 | ##### mutators 157 | 158 | Type: `?{ [string]: Function }` 159 | 160 | See [FormApi.mutators](https://github.com/final-form/final-form#mutators--string-function-). 161 | 162 | ##### batch 163 | 164 | Type: `function` 165 | 166 | See [FormApi.batch](https://github.com/final-form/final-form#batch-fn---void--void). 167 | 168 | ##### blur 169 | 170 | Type: `function` 171 | 172 | See [FormApi.blur](https://github.com/final-form/final-form#blur-name-string--void). 173 | 174 | ##### change 175 | 176 | Type: `function` 177 | 178 | See [FormApi.change](https://github.com/final-form/final-form#change-name-string-value-any--void). 179 | 180 | ##### focus 181 | 182 | Type: `function` 183 | 184 | See [FormApi.focus](https://github.com/final-form/final-form#focus-name-string--void) 185 | 186 | ##### initialize 187 | 188 | Type: `function` 189 | 190 | See [FormApi.initialize](https://github.com/final-form/final-form#initialize-values-object--void). 191 | 192 | ### `` 193 | 194 | #### Props 195 | 196 | ##### name 197 | 198 | Type: `string`
199 | Required: `true` 200 | 201 | The name of this field. 202 | 203 | See [name](https://github.com/final-form/final-form#name-string-1). 204 | 205 | ##### validate 206 | 207 | Type: `function` `Array` 208 | 209 | A field-level validation function to validate a single field value. Returns an error if the value is not valid, or undefined if the value is valid. 210 | 211 | See [validate](https://github.com/final-form/final-form#validate-value-any-allvalues-object--any). 212 | 213 | ##### subscription 214 | 215 | Type: `FieldSubscription`
216 | Default: All 217 | 218 | See [FieldSubcription](https://github.com/final-form/final-form#fieldsubscription--string-boolean-). 219 | 220 | #### Events 221 | 222 | ##### change 223 | 224 | Params: 225 | 226 | - `fieldState`: https://github.com/final-form/final-form#fieldstate 227 | 228 | #### Scoped slot props 229 | 230 | It basically exposes [FieldState](https://github.com/final-form/final-form#fieldstate). 231 | 232 | ##### name 233 | 234 | Type: `string` 235 | 236 | The name of this field. 237 | 238 | See [`FieldState.name`](https://github.com/final-form/final-form#name-string) 239 | 240 | ##### change 241 | 242 | Type: `function` 243 | 244 | See [`FieldState.change`](https://github.com/final-form/final-form#change-value-any--void) 245 | 246 | ##### value 247 | 248 | Type: `any`. 249 | 250 | The current value of this field. You should probably bind it to `:value` of `input` or `textarea` if you have set initial value for the field. 251 | 252 | ##### events 253 | 254 | Type: `{ input: Function, focus: Function, blur: Function }` 255 | 256 | Bind these event handlers to your `input` `textarea` element. 257 | 258 | See [FieldState.change](https://github.com/final-form/final-form#change-value-any--void), [FieldState.focus](https://github.com/final-form/final-form#focus---void), [FieldState.blur](https://github.com/final-form/final-form#blur---void). 259 | 260 | ##### meta 261 | 262 | Type: `object` 263 | 264 | Everything in [FieldState](https://github.com/final-form/final-form#fieldstate) except for `blur` `change` `focus` `name` 265 | 266 | ## Contributing 267 | 268 | 1. Fork it! 269 | 2. Create your feature branch: `git checkout -b my-new-feature` 270 | 3. Commit your changes: `git commit -am 'Add some feature'` 271 | 4. Push to the branch: `git push origin my-new-feature` 272 | 5. Submit a pull request :D 273 | 274 | ## Author 275 | 276 | **vue-final-form** © [EGOIST](https://github.com/egoist), Released under the [MIT](https://github.com/egoist/vue-final-form/blob/master/LICENSE) License.
277 | Authored and maintained by EGOIST with help from contributors ([list](https://github.com/egoist/vue-final-form/contributors)). 278 | 279 | > [egoist.moe](https://egoist.moe) · GitHub [@EGOIST](https://github.com/egoist) · Twitter [@\_egoistlily](https://twitter.com/_egoistlily) 280 | --------------------------------------------------------------------------------