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

@de-formed/base

v4.1.3

Published

Base Validation Class to generate validations for various implementations of @De-Formed.

Downloads

73

Readme

npm version Known Vulnerabilities example workflow codecov size

@De-Formed Validations offers a highly customizable API to create form and data validations. The functions in this library are aimed at unifying the implementation of @De-Formed for targeted JavaScript libraries or frameworks. Use these to either create an implementation for a library that doesn't exist yet or to create your own variant that suits your needs.

@De-Formed is used and trusted in production at Google and Microsoft.

Why Use De-Formed?

  1. Modular - decoupled from your form architecture.
  2. Composable - turn your validations and forms into Lego bricks.
  3. Extendable - add/modify the API as you see fit
  4. Unopinionated - customize your UX to the Moon 🚀
  5. Lightweight - compare it on bundlephobia
  6. Easy to Use - functions all the way down
  7. Easy to Test - unit test your validation rules
  8. Yup Compatible - can integrate with your existing yup schemas

Install

yarn add @de-formed/base
npm i @de-formed/base

Current Implementations

This repository is to generate customized De-Formed variants. If you are looking for an existing solution, please visit one of the links below.


Validation Schema

The validation schema is on object that defines a list of validation rules for any given key. Each validation rule consists of the error to display to a user and a function that returns true or false. Error messages can be passed a function to generate dynamic error messages depending on the state of the data.

{
  email: [
    {
      error: 'Email is required.',
      validation: ({ email }) => email.trim().length > 0,
    },
    {
    error: ({ email }) => `"${email}" is not a valid email.`,
      validation: ({ email, name }) =>
        name === 'bob ross' ? email === '[email protected]' : true
    },
  ],
}

To instantiate @De-Formed, pass a validation schema to the constructor. We highly recommend wrapping your schema constructors in a function call. This will make it easier to compose and reuse across your application:

import { useValidation } from '@de-formed/react-validations'

cosnt personValidation = () => {
  return useValidation<Person>({
    email: [
      {
        error: 'Email is required.',
        validation: ({ email }) => email.trim().length > 0,
      },
      {
        error: ({ email }) => `"${email}" is not a valid email.`,
        validation: ({ email, name }) =>
          name === 'bob ross' ? email === '[email protected]' : true,
      },
    ],
  })
}

Conditional Validation with Polymorphic Types

There are times where a single form can encapsulate the creation or editing of different domain types that are grouped into a super type, such as a Pet form that could be for various types of pets with different validation requirements:

// schema using the React Hook
export const petValidationSchema = () => {
  return useValidation<Pet>({
    favoriteChewToy: [
      {
        error: 'Favorite Chew Toy is required.',
        validation: (pet) => (isDog(pet) ? !!pet.favoriteChewToy : true),
      },
    ],
    sleepingHabits: [
      {
        error: 'Sleeping Habits is required.',
        validation: (pet) => (isCat(pet) ? !!pet.sleepingHabits : true),
      },
    ],
    isDancing: [
      {
        error: 'Crabs should always be dancing',
        validation: (pet) => (isCrab(pet) ? pet.isDancing : true),
      },
    ],
  })
}

Flexible Schema Definitions

The @De-Formed schema is designed to be logical but flexible. The type provided to the constructor does not restrict the rules that can be applied. This allows developers to handle complex validation requirements with various approaches.

// our Blog type
type Blog = {
  title: string
  author: string
  content: string
  terms: boolean
  status: 'draft' | 'published'
}

Requirements for publishing a blog vs auto-saving:

Approach #1 -- create a schema that defines our publishing validations but has an additional rule for auto-saving:

import { required, is } from '@de-formed/base'
import { useValidation } from '@de-formed/react-validations'

// react hook example with auto-props
const useBlogValidation = () => {
  return useValidation<Blog>({
    title: [required()],
    author: [required()],
    content: [required()],
    terms: [is(true)],
    canAutoSave: [ // <-- notice this key does not exist in the Blog type
      {
        error: 'Please provide a title before saving your progress',
        validation: ({ title, status }) =>
          title.trim().length > 0 && status === 'draft',
      },
    ],
  })
}
// inside a React Component
const { validate } = useBlogValidation()

const autoSave = () => {
  if (validate('canAutoSave', blog)) {
    // auto save logic
  }
}

const publish = () => {
  // use the overloads for validateAll to call the validations for publishing
  if (validateAll(blog, ['title', 'author', 'content', 'terms'])) {
    // publish blog logic
  }
}

This approach might make the most sense in some scenarios, but an alternative might be to compose our blog validation with another schema to handle auto-saving versus publishing.

Approach #2 -- compose validation requirements into two schemas

Note that this example is using Auto-Props

const useBlogValidation = () => {
  return useValidation<Blog>({
    title: [required()],
    author: [required()],
    content: [required()],
    terms: [is(true)],
  })
}
// new schema with dedicated auto-save and publish validations composed with
// blog validations
const useBlogSubmitValidation = () => {
  const { validateAll } = useBlogValidation()

  return useValidation<Blog>({
    canAutoSave: [
      {
        error: 'Blogs must be in draft and contain a title to be saved.',
        validation: ({ title, status }) =>
          title.trim().length > 0 && status === 'draft',
      },
    ],
    canPublish: [
      {
        error: 'Not all requirements have been met for publishing.',
        validation: (blog) => validateAll(blog),
      },
    ],
  })
}
// inside a React Component
const { validate } = useBlogSubmitValidation()

const autoSave = () => {
  if (validate('canAutoSave', blog)) {
    // auto save logic
  }
}

const publish = () => {
  if (validate('canPublish', blog)) {
    // publish blog logic
  }
}

An advantage here is that the rules are now more declarative and self-documenting. It is clear to see A) what is a valid blog, B) what are the requirements to auto-save, and C) what are the requirements to publish. All of these requirements encapsulated within a hook and easily re-shared with other components that might need to do similar validation checks.


Composing Forms with Validations

In the previous example, we showed how you can be more expressive with composition as validation requirements become more complex. However, if all we communicated to a user was Not all requirements have been met for publishing. we would be providing a very poor experience. However, we can compose forms just the same as we composed our validation schemas.

To do this, we will create two different abstractions:

  • a Blog Form
  • a Blog Controller

The form will contain our blog validations and provide user feedback, while the blog controller will contain auto-saving and publishing logic. We can then communicate to our form when it needs to display all errors to a user due to an event outside the form itself.

// BlogController.tsx
const BlogController = () => {
  const [blog, setBlog] = React.useState<Blog>({
    /** initial blog state **/
  })
  const [publishFailed, setPublishFailed] = React.useState<boolean>(false)
  const { getError, validate } = useBlogSubmitValidation()

  const onChange = (data: Partial<Blog>) =>
    setBlog((prev) => ({ ...prev, ...data }))

  const publish = () => {
    if (validate('canPublish', blog)) {
      setPublishFailed(false)
      // publish blog logic
    } else {
      setPublishFailed(true)
    }
  }

  return (
    <>
      <h2>Edit Blog</h2>
      <div role="form">
        <BlogForm
          data={blog}
          onChange={onChange}
          publishFailed={publishFailed}
        />
        <button onClick={publish}>Publish Blog</button>
        {getError('canPublish') && <p>{getError('canPublish')}</p>}
      </div>
    </>
  )
}
// BlogForm.tsx
const BlogForm = ({ data, onChange, publishFailed }) => {
  // instantiate our blog validations
  const { getError, validateOnChange, validateOnBlur, validateAll } =
    useBlogValidation()

  // create an onchange handler that can transform an event into a partial
  const handleChange = (event) =>
    onChange({
      [event.target.name]: [event.target.value],
    })

  // listen for publish failed events
  React.useEffect(() => {
    if(publishFailed) validateAll(data)
  }, [publishFailed])

  return (
    <>
      <div>
        <label htmlFor="title">Blog Title</label>
        <input
          id="title"
          name="title"
          onBlur={validateOnBlur(data)}
          onChange={validateOnChange(handleChange, data)}
          value={data.title}
        />
        {getError('title') && <p>{getError('title')}</p>}
      </div>
      <div>
        <label htmlFor="author">Author</label>
        <input
          id="author"
          name="author"
          onBlur={validateOnBlur(data)}
          onChange={validateOnChange(handleChange, data)}
          value={data.author}
        />
        {getError('author') && <p>{getError('author')}</p>}
      </div>
      <div>
        <label htmlFor="content">Content</label>
        <input
          id="content"
          name="content"
          onBlur={validateOnBlur(data)}
          onChange={validateOnChange(handleChange, data)}
          value={data.content}
        />
        {getError('content') && <p>{getError('content')}</p>}
      </div>
    </>
  )
}

Now when a publish event fails, the child form is notified to run its validations to provide feedback to the user about what specifically failed. This pattern can be used to generate form partials that can be reused and composed in as many parts of the application as necessary.

In addition, all of the form logic is decoupled from the validation logic. @De-Formed can be easily removed or updated with different validation requirements without affecting the data flow of the form.

Lastly, customizing the behavior of the validations is the most important aspect of creating a great user experience. @De-Formed provides complete control over how you wish validation behavior to occur. For example, if we don't want content validations to fire onBlur events, we simply remove the binding on the content input.

Here is an extended codesandbox example that kicks out the jams on what you can do with composable forms when the need arises.


A Quick Note on Form Tags

We do not use the semantic <form> tag in our examples. Form tags make re-using and composing forms difficult as it is invalid for a form tag to be nested within another form tag. Furthermore, form tags do not provide any necessary accessibility for the web. Form tags are treated by assistive technology as landmarks that can be jumped to (much like ); however, these roles are mostly useful in cases where you want to make a form an explicit landmark. There is no a11y requirement that all forms need to be landmarks, but if you wish for a form to be, you can wrap a container div containing the child form(s) with a role="form" tag. However, this simply tells a screen reader there is a form region. There are numerous ways to provide enhanced a11y for declaring regions than purely through declaring a region as a generic entity, especially if there are multiple forms on a page. This might seem polemical to challenge the notion of semantic HTML but not all useful regions have semantic HTML tags to accompany them (e.g., role="banner" or role="search") and so it is important to explicitly think about how best to convey to assistive technology regions that are important for a user to quickly navigate. More on web accessibility for forms.


Co-Dependent Validations

Because @De-Formed provides complete control over what validations you want to run at any given event, creating user experiences with co-dependent validations is a breeze.

Let's take an example where we have a multi-input form element that selects a type of measurement, with a second input that chooses a particular value. Our validation requirements are pretty strict in this case, where depending on the measurement, only some values are acceptable.

type Quanity = {
  measurement: 'height' | 'length' | 'width'
  value: number
  // imagine there are additional properties but these are the two that are co-dependent
}
const useQuantityValidation = () => {
  return useValidation<Quantity>({
    value: [
      {
        error: 'height must be between 4 and 20',
        validation: ({ measurement, value }) =>
          measurement === 'height' ? value >= 4 <= 20 : true
      },
      {
        error: 'length must be between 8 and 42',
        validation: ({ measurement, value }) =>
          measurement === 'length' ? value >= 8 <= 42 : true
      },
      {
        error: 'width must be between 10 and 12',
        validation: ({ measurement, value }) =>
          measurement === 'width' ? value >= 10 <= 12 : true
      },
    ]
  }),
}

Each time a user changes the measurement select box, we want to run the value validations so that they don't continue with the form until the value is valid according to the measurement selected:

const QuantityForm = () => {
  const [quantity, setQuantity] = React.useState<Quantity>({
    measurement: ''
    value: 0
  })

  // we are going to forgo the auto-magic methods and build our own use case
  const { getError, validate, validateIfDirty } = useQuantityValidation()

  const handleChange = (data: Partial<Quantity>) =>
    setQuantity(prev => ({ ...prev, ...data }))

  const validateOnChange = (event) => {
    cosnt updated = { ...quantity, [event.target.name]: event.target.value }
    if (event.target.name === 'measurement') {
      validateAllIfDirty(updated, ['measurement', 'value'])
    } else {
      validateIfDirty(event.target.name, updated)
    }
    onChange(updated)
  }

  const validateOnBlur = (event) => {
    cosnt updated = { ...quantity, [event.target.name]: event.target.value }
    if (event.target.name === 'measurement') {
      validateAll(updated, ['measurement', 'value'])
    } else {
      validate(event.target.name, updated)
    }
  }

  return (
    <>
      <select
        name="measurement"
        onBlur={ValidateOnBlur}
        onChange={validateOnChange}
        value={quantity.measurement}
      >
        <option value="height">Height</option>
        <option value="length">Length</option>
        <option value="width">Width</option>
      </select>
      <input
        name="value"
        onBlur={validateOnBlur}
        onChange={validateOnChange}
        type="number"
        value={quantity.value}
      />
      {getError('value') && <p>{getError('value')</p>}}
      {/** other inputs **/}
    </>
  )
}

Under the hood, validateOnChange and validateOnBlur run validateIfDirty and validate respectively, but just snag the event name from the event to look up what validations it should run. Here, we leveraged the same idea but customized it so that one particular event name always fires two validations.

The important point here is that no matter what the UX of your validations you want to create is, @De-Formed gives you tremendous customization options.


Yup Compatible

const schema = Yup.object({
  name: Yup.string()
    .required('Name is required.')
    .test({
      message: 'Cannot be bob.',
      test: (value: string | undefined) => value !== 'bob',
    })
    .when('dingo', {
      is: true,
      then: Yup.string().test({
        message: 'Must be dingo.',
        test: (value: string | undefined) => value === 'dingo',
      }),
    }),
  age: Yup.number().test({
    message: 'Must be 42 or older.',
    test: (value: number | undefined) => (value ? value >= 42 : false),
  }),
  agreement: Yup.boolean().isTrue('Must accept terms'),
})

If you are already using Yup or wish to use its schema design, simply pass your Yup schema to @De-Formed with the following config option:

const v = Validation(schema, { yup: true })

Auto-Props

Auto-props are functions that apply simple validation rules for strings and numbers.

Auto-props only work for keys of a schema that match the name of a property on the state.

type Person = {
  name: string
  age: number
  agreement: boolean
}

// valid use of auto-props
const personValidation = () => {
  return Validation<Person>({
    name: [required(), shorterThan(12)],
    age: [min(42), max(100)],
    agreement: [is(true, 'Must accept terms.')],
  })
}

// invalid use of auto-props
const personValidation = () => {
  return Validation<Person>({
    name: [required(), shorterThan(12)],
    age: [min(42), max(100)],
    agreement: [is(true, 'Must accept terms.')],
    isAwesome: [required()], // <-- `isAwesome` doesn't exist on a Person object
  })
}

Auto-props are ignored when the key they are defined on does not exist on the object being validated. The reason for this is that under the hood, auto-props are passed the value of a property from the object state directly and are not intended to handle conditional validations which custom keys are intended for. Because auto-props are designed to also handle optional properties, the only way to handle undefined as a value versus undefined as a missing property is to ignore the auto-prop if the target property is not defined on the object being validated.

// invalid schema fixed
const personValidation = () => {
  return Validation<Person>({
    name: [required(), shorterThan(12)],
    age: [min(42), max(100)],
    agreement: [is(true, 'Must accept terms.')],
    isAwesome: [
      {
        error: ({ name }) => `${name} is not awesome.`,
        validation: ({ name }) => name === 'Bob Ross',
      },
    ],
  })
}

Available Auto-props are:

  • required (string/number)
  • matches (string)
  • shorterThan (string)
  • longerThan (string)
  • min (number)
  • max (number)
  • is (any type, also accepts a predicate function)

Auto-props are great for handling very simple rules, however more complex validations should use the explicit schema format:

const PersonValidation = () =>
  Validation<Person>({
    name: [
      required(),
      {
        error: 'Cannot be bob.',
        validation: ({ name }) => name !== 'bob',
      },
      {
        error: ({ name }) => `${name} must be dingo.`,
        validation: ({ dingo, name }) => (dingo ? name === 'dingo' : true),
      },
    ],
    age: [min(42, 'Must be 42 or older.')],
    agreement: [is(true, 'Must accept terms')],
  })

Rendering Server Errors to a UI

Developers using a Node backend can use the Node implementation to easily share errors with the frontend to render errors directly onto the inputs:

// example Node controller logic to save a pet
const createNewPet = async (pet: Pet) => {
  // instantiate a fresh validation state
  const v = Validation<Pet>({
    license: [matches(/some-regex/), 'License must be valid'],
    exists: [is(false, 'Pet already exists')],
  })
  const existing = await Pet.find(pet)
  v.validateAll({ license: pet.license, exists: Boolean(existing) })
  if (v.isValid) {
    const newPet = await Pet.create(pet)
    return { errors: v.validationState, pet: newPet }
  } else {
    return { errors: v.validationState, pet: null }
  }
}
// example React UI to render API errors directly on inputs
const { validateAll, setValidationState } = usePetValidation()

const handleSave = () => {
  if (validateAll(pet)) {
    savePet(pet).then((response) => {
      if (response.pet) {
        // success -> do happy path
      } else {
        // ruh-roh -> display API errors
        setValidationState(response.errors)
      }
    })
  }
}

If your server isn't built with JavaScript, write a transformation that suitably converts your APIs error payload into a validation state before calling setValidationState to render errors on the DOM by their associated inputs.


Async Validations

All validation functions for @De-Formed are synchronous for performance and simplicity. Validations that require asynchronous logic can be abstracted to a process before running your validation checks.

  • better encapsulation around your validation state
  • allows validation state to be used in control flow
  • requires developers to handle their own blocking application states (e.g., loading, processing, pre-flight checks, etc.).

Internationalization

@De-Formed does not handle i18n internally, however i18n strings can be passed anywhere that @De-Formed takes an error string. Default error messages generated by auto-props are in English.


More on the Validation Object

For a complete rundown of the API, please visit the API Documentation


Creating your own implementation of @De-Formed

There are a number of reasons you might wish to build your own customized Validation API:

  1. An implementation of @De-Formed doesn't work with your target framework
  2. Integration directly with a different state engine (e.g., redux)
  3. Simplify and slim down the API to the only the functionality desired
  4. Extend and add custom functionality to the API not provided
  5. Customize @De-Formed to fit with other dependencies of your application

Perhaps you need @De-Formed to kick off a redux action every time a particular validation fires? Perhaps you have a more preferred state engine you wish to use? Instead of creating a wrapper around the default implementations which creates an unnecessary layer of abstraction, additional performance cost, and additional memory usage, you can simply import the factories and build your own that suits your needs.

Implementing your own is as simple as using one of the current implementations (e.g. @de-formed/react-validations or @de-formed/node-validations) as a template and modify however you see fit. All you need is the factories provided by @de-formed/base.

Factories all the way down

De-Formed is built with factories that accept your state's getter and setter. You can use a default implementation, or build your own and integrate with a state engine of your choosing. If you need further customization, you can modify the factories themselves in index.ts and use the config object to pass around additional settings. Most low-level customizations will only require you to modify the updateProperty however (as example) you may decide that expanding the validation state to contain additional properties is beneficial for your particular needs.

Providing State and a Config object

  • Config is an optional object that can be read anywhere in @De-Formed to modify its behavior.
  • ValidationState can be an object or a function that returns an object
  • SetValidationState is a function that updates the state

How state management is left purely to the implementation. See examples/vanilla.ts for an example.


Adding new implementations of @De-Formed to NPM

If there is no current implementation that works for your framework in the @De-Formed ecosystem you can open a feature request with a PR containing your implementation to make it available for others. The current ValidationObject type is the intended de-facto implementation and should be adhered to for consistency. If this object is missing functionality you think would benefit @De-Formed, please make a feature request and provide an example of what you would like to be available. Please keep in mind, while enhancements will be eagerly reviewed, a huge motivation for @De-Formed is to keep it small (~1-2kb) and simple.


License

This project is licensed under the terms of the MIT license.