npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@formstate/core

v0.1.5

Published

formstate is a small vue3 library that makes it easy to handle your form state and perform validations. It features:

Downloads

21

Readme

formstate

formstate is a small vue3 library that makes it easy to handle your form state and perform validations. It features:

  • A small footprint (2.7kb gzip)
  • No tight coupling with inputs, you have all the liberty to build your own inputs
  • Full Typescript support, with returned fields typed based on your initial state
  • Easy zod or other validation libraries support
  • You can directly manipulate your field values (e.g. value, dirty, focused etc)
  • Shared state of useForm -> you can reuse the same form in another component by naming your form
  • Async validation support
  • Custom error objects, you can return any object from a validation rule

Installation

For a vue 3 project:

npm install @formstate/core
yarn add @formstate/core
pnpm add @formstate/core

For nuxt:

npm install @formstate/core @formstate/nuxt
yarn add @formstate/core @formstate/nuxt
pnpm add @formstate/core @formstate/nuxt

And add the @formstate/nuxt package to your modules in nuxt.config.ts:

export default defineNuxtConfig({
  devtools: { enabled: true },
  modules: ["@formstate/nuxt"],
});

Usage

<script lang="ts" setup>
import { useForm } from "@formstate/core";

const { someText, formState } = useForm({
  someText: "initial value",
});

</script>

<template>
  <input type="text" v-model="someText.value" />
</template>
  <div>Input is dirty: {{ someText.dirty }}</div>
  <div>Input is valid: {{ someText.valid }}</div>
  <div>Complete state: {{ formState }}</div>

The formState ref contains the complete state of the form:

console.log(formState.value)

// result:
 {
      "context": {},
      "dirty": false,
      "fields": {
        "someText": {
          "blur": [Function],
          "dirty": false,
          "errors": [],
          "focus": [Function],
          "focused": false,
          "name": "someText",
          "pending": false,
          "reset": [Function],
          "touched": false,
          "valid": true,
          "validate": [Function],
          "value": "initial value",
        },
      },
      "initialFields": {
        "numberInput": {
          "rules": [Function],
          "value": 2,
        },
        "someText": "hello",
      },
      "pending": false,
      "touched": false,
      "valid": true,
    }

We can also get all the inputs with their values from the form as a ref:

const { values } = useForm({
  someText: "initial value",
  checkboxes: ["Jack"],
});

console.log(values.value);
// output: { "someText": "initial value", "checkbox": ["Jack"] }

Validation

Rules

Anything you return from a rule function means that the field is invalid. If you return nothing, undefined or false, it will be considered valid.

// if you false, it will generate an error message of required
const isRequired = (value: string) => !!value;

function rule(value: string, fieldName: string) {
  if (value.length > 5) {
    return "requires more than 5 characters";
  }
  // or return array
  if (value.length > 5) {
    return ["requires more than 5 characters", "some other error"];
  }

  // or any custom object
  if (value.length > 5) {
    return { error: "requires more than 5 characters", code: "too short" };
  }
}

// or do something like this
function rule(value: string) {
  let errors: string[] = [];
  if (value.length < 5) {
    errors.push("min 5 chars");
  }
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
    errors.push("invalid email");
  }
  return errors;
}

We can use the setRules function to add rules in one go:

const { someRadio, someTextInput, formState, setRules } = useForm({
  someText: ""
  someRadio: "John",
});


setRules( {
  someTextInput: [isRequired, validateIfJohn],
  someRadio: [validateIfJohn],
});

You can also set the rules directly on the fields:

import { isRequired, isNotJohn } from '../some-validations'

const { someRadio } = useForm({
  someText: ""
  someRadio: "John",
});

someText.rules = [isRequired, isNotJohn]


</script>

<template>
  <label for="John">John</label>
  <input id="John" type="radio" v-model="someRadio.value" value="John" />
  <label for="John">Jack</label>
  <input id="Jack" type="radio" v-model="someRadio.value" value="Jack" />

  <div>Error messages: {{ someRadio.errors }}</div>
</template>

Adding a rule later on

We can add a rule later on as well:

// set a new array on .rules, push doesnt work
someText.rules = [...someText.rules, someOtherValidation];

When are rules validated?

  • If no rule is present, the field is considered valid
  • By default rules are validated when a field value changes
  • You can customize when a validation is performed, see // TODO add link to custom validation behaviour

Cross field validation and validating complete form

Sometimes we want to validate a group of fields, or have rules depend on each other. You can set the formRules property on the formState object to do this. Form rules work in addition to field rules and produce extra errors.

const formRule = () => {
  if (someTextInput.value === "John" && someRadio.value === "John") {
    return "John is not allowed on both fields";
  }
};

formState.value.formRules = [formRule];

// or

setRules({
  someTextInput: [required],
  formRules: [formRule],
});

Zod (or other validation libraries)

Because validations are just simple functions, you can use any validation library you'd like

const MyFormValidation = z.object({
  someTextInput: z.string().min(1),
  someRadio: z.string(),
  someRange: z.union([z.string(), z.number()]),
});

type MyForm = z.infer<typeof MyFormValidation>;

// formState type is defered from zod
const { formState, setRules } = useForm<MyForm>({
  someTextInput: "",
  someRadio: "John",
  someRange: 5,
});

const zodValidation = (value: unknown, name: string) => {
  // we pick the value from zod, as we don't want to validate whole formstate
  const result = ZodType.pick({ [name]: true }).safeParse({ [name]: value });
  if (!result.success) {
    return result.error.errors;
  }
};

setRules({
  someTextInput: [zodValidation],
  someRadio: [zodValidation],
  someRange: [zodValidation],
});

Async validation

formstate has async validation built in. You can add an async function to a field or use async validation as a form rule.

const userNameExists = async (value: string) => {
  const exists = await callToApi(value);
  if (exists) {
    return "Username already exists";
  }
};

setRules({ username: [userNameExists] });

Debounce async validation

Most often don't want to do a request on each change of a text field, as it would mean many calls to your api. You can debounce a rule, as long as the rule always returns a promise.

import debounce from "debounce-promise";

const someAsyncValidation = debounce(async (value: string) => {
  // if length is less than 5 characters, don't do a request
  if (value.length < 5) {
    return "username should be longer than 5 characters";
  }

  const exists = await callToApi(value);
  if (exists) {
    return "Username already exists";
  }
}, 500);

setRules({ username: [someAsyncValidation] });

Turning off autovalidation

By default, validations on a field are performed when the field value changes. You can turn off this behaviour and write your own implementation when validation is performed:

setRules({
  someTextInput: [{ rule: someAsyncValidation, autoValidate: false }],
});

watch(
  () => someTextInput.focused,
  async () => {
    if (!someTextInput.focused) {
      await someTextInput.validate();
    }
  }
);

Using lazy v-model

TODO

Changing field and form values

Field values

const { someText, setFields } = useForm({
  someText: "",
});

// editing .value will change dirty state and perform validation
someText.value = "hello world!";

// using setFields function will not change dirty or perform validation
setFields({ someText: "hello world!" });

// you can also change this behavior:
setFields({ someText: "hello world!" }, { setDirty: true, validate: true });

// other values can also be changed
someText.dirty = true;
someText.touched = true;
someText.focused = true;
someText.pending = true;
someText.valid = true;

Form values

const { formState } = useForm({
  someText: "",
});

formState.value.dirty = true;
formState.value.pending = true;
formState.value.touched = true;
formState.value.valid = true;

Populating async data

Often we would like to populate a form from an async request. The easiest way is to wrap the component were the form is located in a Suspense and async/await for the data:

const data: ResponseType = await anAsyncRequest();

const { someInput } = useForm(data);

We can also use the setFields function to later update the data

const { numberInput, textInput, setFields } = useForm({
  numberInput: 0,
  textInput: "",
});

onMounted(async () => {
  // data would be something like { numberInput: 6, textInput: 'hello world' }
  const data: ResponseType = await anAsyncRequest();
  setFields(data);
});

Adding focus and blur functions

Every input has touched and boolean properties. But in order to automatically update these properties we need to attach the input events to the formState. Note that in some browsers (looking at you safari), some types of inputs (e.g. radio) do not trigger the focus events.

<script lang="ts" setup>
import { useForm, Input } from "@formstate/core";

const { someText } = useForm({
  someText: "initial value",
});

</script>

<template>
  <input
    type="text"
    v-model="someText.value"
    @focus="someText.focus"
    @blur="someText.blur"
  />

  <div>Input is focused: {{ someText.focused }}</div>
  <div>Input has been touched: {{ someText.touched }}</div>
</template>

Submitting

Submitting your form can be as simple as:

const { values } = useForm({ someTextInput: "hello" });

async function submit() {
   const { valid, errors, errorFields } = await validateForm();
   if (valid) {
      await yourPostRequest(values);
   }
}
<template>
 <button @click="sumbit">Submit</button>
</template>

Or you can choose to use the <form> element:


<template>
 <form @submit.prevent="sumbit">
    <button>Submit</button>
  </form>
</template>

Reset form and fields

To reset the complete form to its initial state, you can use the resetForm function:

const { resetForm } = useForm({ someText: "" });

<template>
  <button @click="resetForm">Reset form</button>
</template>

Reset individual fields

const { someText } = useForm({ someText: "" });

<template>
  <button @click="someText.reset">Reset text field</button>
</template>

Array values

Using array values in formstate

In order to dynamically create form elements you can add an array as input

  const { someArray } = useForm({
    someArray: [
    { name: "somename", email: "" },
    { name: "", email: "" },
  ]});

  <fieldset v-for="(v, i) of someArray.value" :key="i">
    <legend>array</legend>
    <input
      type="text"
      v-model="v.name"
    />
    <input
      type="text"
      v-model="v.email"
    />
  </fieldset>

Adding and removing array values

You can directly modify the array (push, pop). But in order to not directly trigger validation everytime, it is better to update using the setFields function.

function addToArray() {
  setFields({
    someArray: [...someArray.value, { name: "", email: "" }],
  });
}
function remove(index: number) {
  setFields({
    someArray: someArray.value.filter((_, i) => i !== index),
  });
}

 <fieldset v-for="(v, i) of someArray.value" :key="i">
    <legend>array</legend>
    <input
      type="text"
      v-model="v.name"
    />
    <input
      type="text"
      v-model="v.email"
    />
    <button @click="remove(i)">Remove</button>
  </fieldset>


<button @click="addToArray">Add to array</button>

Validating array fields

const ZodType = z.object({
  someArray: z.array(
    z.object({
      name: z.string().min(3),
      email: z.string().email(),
    })
  ),
});

const zodValidation = (value: unknown, name: string) => {
  // we pick the value from zod, as we don't want to validate whole formstate
  const result = ZodType.pick({ [name]: true }).safeParse({ [name]: value });
  if (!result.success) {
    return result.error.errors;
  }
};

setRules({
  someArray: [zodValidation],
});

<fieldset v-for="(v, i) of someArray.value" :key="i">
  <legend>array</legend>
  <input
    type="text"
    v-model="v.name"
    :class="{
      invalid: someArray.errors.filter((e) => e.path[1] === i && e.path[2] === 'name').length > 0,
    }"
  />
  <input
    type="text"
    v-model="v.email"
    :class="{
      invalid: someArray.errors.filter((e) => e.path[1] === i && e.path[2] === 'email').length > 0,
    }"
  />
</fieldset>

Reusing form

TODO