Giter VIP home page Giter VIP logo

slimeform's Introduction




SlimeForm

English | 简体中文

npm Test docs


Form state management and validation

Why?

We 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.

Base on modern CSS utilities class and component-based design, it has now become way more easier to write your own <input> 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.

So 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 💙.

SlimeForm 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.

TODO

  • Improve the functionalities
    • Use reactive type to return the form
    • For a single rule, the array can be omitted
    • Mark whether the value of the form has been modified
  • Documentations
  • Better type definitions for Typescript
  • Unit tests
  • Add support to fields with object type
  • Add support to async rule validation
  • Support filtering the unmodified entries in the form, leaving only the modified entries for submission
  • Support for third-party rules, such as yup
    • Support validateSync
    • Support validate (Async)
  • 💡 More ideas...

Contributions are welcomed

Try it online

🚀 slimeform-playground

Install

⚗️ Experimental

npm i slimeform

SlimeForm only works with Vue 3

Usage

Form state management

Use v-model to bind form[key] on to the <input> element or other components.

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.

<script setup>
import { useForm } from 'slimeform'

const { form, status, reset, dirtyFields } = useForm({
  // Initial form value
  form: () => ({
    username: '',
    password: '',
  }),
})
</script>

<template>
  <form @submit.prevent="mySubmit">
    <label>
      <!-- here -->
      <input v-model="form.username" type="text">
      <input v-model="form.password" type="text">
    </label>
    <button type="submit">
      Submit
    </button>
  </form>
</template>

State management

const { form, status, reset, isDirty } = useForm(/* ... */)

// whether the form has been modified
isDirty.value
// whether the username has been modified
status.username.isDirty
// whether the password has been modified
status.password.isDirty

// Reset form, restore form values to default
reset()

// Reset the specified fields
reset('username', 'password', /* ... */)

Mutable initial value of form

The 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.

const userStore = useUserStore()

const { form, reset } = useForm({
  form: () => ({
    username: userStore.username,
    intro: userStore.intro,
  }),
})

// update the value of username and intro properties
userStore.setInfo(/* ... */)
// changes made to the `userStore` will be synced into the `form` object,
// when reset is being called
reset()

// these properties will be the values of `userStore` where `setInfo` has been called previously
form.username
form.intro

Filtering out modified fields

Suppose 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 HTTP 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

Such a requirement can use the dirtyFields computed function, whose value is an object that only contains the modified fields in the form.

const { form: userInfo, status, dirtyFields } = useForm(/* ... */)

dirtyFields.value /* value: {} */

// Edit user intro
userInfo.intro = 'abcd'

dirtyFields.value /* value: { intro: 'abcd' } */

// Edit user profile to default
userInfo.intro = '' /* default value */

dirtyFields.value /* value: {} */

Validating rules for form

Use 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.

You can also maintain your rule collections on your own, and import them where they are needed.

// formRules.ts
function isRequired(value) {
  if (value && value.trim())
    return true

  return t('required') // i18n support
}
<script setup>
import { isRequired } from '~/util/formRules.ts'
const {
  form,
  status,
  submitter,
  clearErrors,
  isError,
  verify
} = useForm({
  // Initial form value
  form: () => ({
    name: '',
    age: '',
  }),
  // Verification rules
  rule: {
    name: isRequired,
    // If one fields requires more then one rule, it can be declared by using function arrays.
    age: [
      isRequired,
      // is number
      val => !Number.isNaN(val) || 'Expected number',
      // max length
      val => val.length < 3 || 'Length needs to be less than 3',
    ],
  },
})

const { submit } = submitter(() => {
  alert(`Age: ${form.age} \n Name: ${form.name}`)
})
</script>

<template>
  <form @submit.prevent="submit">
    <!-- ... -->
  </form>
</template>

In 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.

Manually trigger the validation

const { _, status, verify } = useForm(/* ... */)
// validate the form
verify()
// validate individual fields
status.username.verify()

Manually specify error message

status.username.setError('username has been registered')

Maunally clear the errors

const { _, status, clearErrors, reset } = useForm(/* ... */)
// clear the error for individual field
status.username.clearError()
// clear all the errors
clearErrors()
// reset will also clear the errors
reset()

Any errors

isError: Are there any form fields that contain incorrect validation results

const { _, isError } = useForm(/* ... */)

isError /* true / false */

Default message for form

Use 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 &nbsp; during rendering, to avoid the height collapse problem of <p> when there is no messages.

const { form, status } = useForm({
  form: () => ({/* ... */}),
  rule: {/* ... */},
  // Placeholder content when there are no error message
  defaultMessage: '\u00A0',
})

Lazy rule validation

You can set lazy to true to prevent rules from being automatically verified when data changes.

In this case, consider call verify() or status[fieldName].verify() to manually validate fields.

const { form, status, verify } = useForm({
  form: () => ({
    userName: '',
    /* ... */
  }),

  rule: {
    userName: v => v.length < 3,
  },

  lazy: true,
})

form.userName = 'abc'
status.userName.isError // false

verify()

status.userName.isError // true

rule in return value of useForm()

Slimeform 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.

const { form, rule } = useForm({
  form: () => ({
    userName: '',
    /* ... */
  }),

  rule: {
    userName: v => v.length < 3 || 'to many characters',
  },
})

const text = 'abcd'
const isValid = rule.userName.validate(text) // false
if (isValid)
  form.userName = text

You 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.

rule.userName.validate('abcd', { fullResult: true }) // { valid: false, message: "to many characters" }
rule.userName.validate('abc', { fullResult: true }) // { valid: true, message: null }

Submission

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.

<script setup>
import { useForm } from 'slimeform'

const { _, submitter } = useForm(/* ... */)

// Define the submit function
const {
  // trigger submit callback
  submit,
  // Indicates whether the asynchronous commit function is executing
  submitting,
} = submitter(async ({ form, status, isError, reset /* ... */ }) => {
  // Submission Code
  const res = await fetch(/* ... */)
  // ....
})
</script>

<template>
  <form @submit.prevent="submit">
    <!-- ... -->

    <!-- Use `submitting` to disable buttons and add loading indicator -->
    <button type="submit" :disabled="submitting">
      <icon v-if="submitting" name="loading" />
      Submit
    </button>
  </form>
</template>

By 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. If 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.

Wrap the generic submission code and use it later

import { mySubmitForm } from './myFetch.ts'
const { _, submitter } = useForm(/* ... */)
// Wrap the generic submission code and use it later
const { submit, submitting } = submitter(mySubmitForm({ url: '/register', method: 'POST' }))

Integrations

Using Yup as a rule

If you don't want to write the details of validation rules yourself, there is already a very clean way to use Yup as a rule.

SlimeForm has a built-in resolvers for 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.

First, you have to install Yup

npm install yup

then import yup and yupFieldRule into your code and you're ready to go!

import { useForm } from 'slimeform'
import * as yup from 'yup'

/* Importing a resolvers */
import { yupFieldRule } from 'slimeform/resolvers'

const { t } = useI18n()

const { form, status } = useForm({
  form: () => ({ age: '' }),
  rule: {
    /* Some use cases */
    age: [
      yupFieldRule(yup.string()
        .required(),
      ),
      yupFieldRule(yup.number()
        .max(120, () => t('xxx_i18n_key'))
        .integer()
        .nullable(),
      ),
    ],
  },
})

Suggestions

Some suggestions:

  1. Use @submit.prevent instead of @submit, this can prevent the submitting action take place by form's default
  2. Use isError to determine whether to add a red border around the form dynamically
<template>
  <h3>Please enter your age</h3>
  <form @submit.prevent="submitFn">
    <label>
      <input
        v-model="form.age"
        type="text"
        :class="status.age.isError && '!border-red'"
      >
      <p>{{ status.age.message }}</p>
    </label>
    <button type="submit">
      Submit
    </button>
  </form>
</template>

slimeform's People

Contributors

allen-1998 avatar littlesound avatar nekomeowww avatar xuzuodong avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

slimeform's Issues

Feature Request: Add `rule` to the return value of `useForm()`

Add rule to the return value of useForm(), so that I can validate a value before assigning it into form:

const { 
    form, 
    rule, // new
} = useForm({
    form: () => {
        return {
           foo: ''
        }
    },
    rule: {
        foo: (v) => !!v.length || 'Foo required!',
    },
})

const myFoo = 'bar'
const isMyFooValid = rule.foo.validate(myFoo) // true
if (isMyFooValid) {
    form.foo = myFoo
}

I think it can be more flexible and convenient in some cases.

feat(useField): rule checking and status management of fields

Rule validation and state management of form fields are placed in a separate input component, which can be used alone or in combination with the main useForm

Code Demo:

Maybe there will be a better way to design?

<!-- MyForm.vue -->
<script setup>
import { useForm, toField } from 'slimeform'
const { form, status, isError } = useForm({
  form: () => ({
    phone: '+86****',
  }),
  rule: {
    phone: [
      // Additional rules for purpose of business logics
    ]
  },
})
</script>

<InputPhone v-model="form.phone" :field="toField(form, 'phone')" />
<!-- InputPhone.vue -->
<script setup>
import { useField } from 'slimeform'
const props = defineProps()
const emits = defineEmits()

const vmPhone = useVModel(props, 'modelValue', emits)

const { field: phone, state, isError, reset } = useField(vmPhone, props.field, {
  rule: [
    // As for mobile phone number input validations, there can be general rules
    phoneFormat(),
    phoneLength(),
  ]
})
</script>

对于 src/index.ts 的更改建议

对于不关心返回值的情况,请使用 unknown 类型。

onSubmit: (callback: () => any) => any

建议改用 jsdoc 格式。

slimeform/src/index.ts

Lines 40 to 43 in 93eaef5

/**
* Submit form
* Verify before submitting, and execute callback if passed
*/

如果不使用 this,改用箭头函数更安全一些。
function verify() {

export function useForm<FormT extends {}>(param: {

if 可优化以减少嵌套。

slimeform/src/index.ts

Lines 92 to 103 in 93eaef5

for (const key in form) {
if (hasOwn(form, key)) {
if (hasOwn(initialForm.value, key)) {
status[key]._ignoreUpdate(() => {
form[key] = (initialForm.value as any)[key] as any
})
}
else {
delete form[key]
}
}
}

变量名意味不明;为什么使用 any?

slimeform/src/index.ts

Lines 159 to 161 in 93eaef5

const fri: RuleItem | RuleItem[] = (formRule as any)?.[key]
if (!fri)
return true

建议使用语义化工具 import('lodash').isFunction
const fieldRules = typeof fri === 'function' ? [fri] : fri

添加一个计算函数 `isError` 返回整个表单当前是否有错误

添加一个计算函数 isError 返回整个表单当前是否有错误,它的值是布尔值,status中任意一个 isErrortrue 它就是 true

const { form, status, isError } = useForm(/* ... */)

isError.value /* false / true */

在现在的实现当中判断一个表单是否无错误需要开发者自己检查 status[keys] 中的每个 isError 值,添加全局的 isError可以解决这个问题

Feature request for a `createForm` function or another way to wrap `useForm`

Hey, I'm building a library where I have a custom form helper that looks exactly like SlimeForm. I think SlimeForm is doing a very good job, and I would love to depend on it for my own form helper.

The issue is that I need to override the submit function with custom logic (this is for a routing library). I wanted to provide my own useForm that would defer the logic to SlimeForm, but I can't get the typings because they're not exported.

I was initially going to PR to export them, but I thought it would be nice if SlimeForm provided some API to generate a pre-configured useForm hook.

I'd happily PR that but I wanted to see if that would be accepted. I'm not sure about the API, but I'm thinking something like that:

// Somewhere in my own library
import { router } from './router'
import type { VisitOptions } from './types'
import { createForm } from 'slimeform'

export const useForm = createForm({
  submit: ({ form }) => async (overrides?: VisitOptions) => {
    // Here I implement my custom submit method, and this method is returned
    return await router.visit({
      method: overrides.method ?? 'POST',
      data: form,
      ...overrides
    })
  },
})
// Somewhere in userland, using my library
import { useForm } from 'my-library'

const { form, submit } = useForm({
  form: () => ({
    email: '',
    password: '',
  })
})

// These parameters are from `VisitOptions`
submit({ url: '/register', method: 'POST' })

Basically, every property from the object given to createForm would be a callback that return a function, and that function would be provided to the user. The callback would have form and status given to it.

It seems a bit complicated, but it's also flexible. If you have another idea on how I could achieve my goal, I'm open to it too!

feat(submitter): better submission

Code Demo:

<script setup>
import { useForm } from 'slimeform' 

const { /* ... */, submitter } = useForm({
  form: () => ({ /* ... */ }),
  rule: { /* ... */ },
})

// Define the submit function
const {
  // trigger submit callback
  submit,
  // Indicates whether the asynchronous commit function is executing
  submitting,
} = submitter(({ form, status, isError, reset /* ... */ }) => {
  // Submit Code
})
</script>

<template>
  <form @submit.prevent="submit">
    <!-- ... -->

    <!-- Use `submitting` to disable buttons and add loading instructions -->
    <button type="submit" :disabled="submitting">
      <icon v-if="submitting" name="loading" />
      Submit
    </button>
  </form>
</template>

Wrap the generic submission function and use the

import { mySubmitForm } from './myFetch.ts'

const { /* ... */, submitter } = useForm(/* ... */)

// Wrap the generic submission code and use the
const { submit, submitting } = submitter(mySubmitForm({ url: '/register', method: 'POST' }))

集成常用的正则校验库来增强易用性

这是一个新的想法,下面用是什么,为什么,怎么做的方式来表达一下我的观点:

  1. 是什么?

    • 集成常用的正则校验库,使用户在需要一些常用的表单校验规则时直接 import 调用即可。
  2. 为什么?

    • 作为一个表单校验库,形影不离的就是表单校验规则
    • 对于正则水平不佳的用户而言会是很有帮助的(无需自己查找、试错,ps:就我接触到的很多甚至几年的前端正则依然一塌糊涂)
    • 就像 vite 会集成 rollup的生态插件,unocss 会集成 tailwind csswindi css的匹配规则
    • 其他的表单库貌似没有实现这一点的(也许是还没想到,也许是因为没必要。。。)
  3. 怎么做?

    • 或许参考一下 antfu 对 icon 的集成方式?(收录常用的并支持自动引入、按需打包)

我个人觉得这个功能对于一部分的用户群体来说是很有帮助的,但是如果确定要做的话具体的实现方式还有待商讨,目前我了解到的一个比较不错的常用正则库是any-rule

Feature Request: disable auto update `status`?

Hello, Dalao!

When using slimeform, is it possible to pass in an options like this:

const { form, status, verify } = useForm({
    form: () => ({
        userName: '',
        ...
    }),

    lazy: true,
})

When lazy is set to true, data updates in form won't automatically cause status to change, unless user call verify() or status.userName.verify() manually.

I think it can be useful if I want to verify content in a Text Input after it's done inputing, i.e. blurred.

Similar behavior can be found here: https://quasar.dev/vue-components/input#example--lazy-rules .

将 nbsp; 设为默认配置

和功能无关,只是一个小建议:

看到文档中推荐使用 nbsp; 来防止内容为空时高度塌陷,个人感觉用 min-height 会不会好一些

image

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.