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

use-model-validation

v0.2.6

Published

Store data in a model and validate that data anywhere

Downloads

73

Readme

Use Model

Store data in a model and validate that data anywhere

Installation

npm add use-model-validation

Example Usage

import { R, createModel } from "use-model-validation";

const model = createModel({
  rules: {
    firstName: [R.max(10, "Too long, must be :max characters or less")],
    lastName: [R.max(20, "Too long, must be :max characters or less")],
    email: [R.required("Email is required")],
    // Supports deeply nested rules
    user: {
      password: [R.required("Password is required")],
    },
  },
});

// Update the model with some data
person.update({ firstName: "James", lastName: "Craig", email: "test" });

// Validate the model, using the model's rules
const { valid, errors, data } = person.validate();

// Set model data (pass an empty object to reset data)
console.log(person.set({}));

Why?

Have you ever ran into the situation where you are having to perform validation on the client and then copy/paste the same validation on the server? This library allows you to define your validation in a single place, then re-use the validation logic anywhere (i.e. on the client and server). Take a look at this, for example:

Define a shared model (/shared/models/user-model.js)

module.exports = createModel({
  rules: {
    firstName: [
      R.required("First name is required"),
      R.max(255, "First name is too large, max characters is :max"),
    ],
    lastName: [
      R.required("Last name is required"),
      R.max(255, "Last name is too large, max characters is :max"),
    ],
    email: [
      R.required("Email name is required"),
      R.email("Email is an invalid format"),
      R.max(255, "Email is too large, max characters is :max"),
    ],
  },
});

Server Route Handler (/server/actions/new-user.js)

const userModel = require("/shared/models/user-model.js");

function newUser(req, res) {
  // Make a fresh instance of the model (to avoid race conditions)
  const user = userModel.fresh(req.body);

  const { valid, errors, data } = user.validate();

  if (!valid) {
    res.status(422).json({ errors });
  }

  // Do something with `data`, e.g. save to DB

  res.status(201).json({ messages: { server: "New user created" } });
}

Client UI (/client/pages/new-user.js)

import userModel from "/shared/models/user-model.js";

function NewUser() {
  const [errors, setErrors] = React.useState({});

  const onChange = React.useCallback((event) => {
    const { name, value } = event.target;
    userModel.update({ [name]: value });
  }, []);

  const onSubmit = React.useCallback(
    async (event) => {
      event.preventDefault();

      const { valid, errors, data } = userModel.validate();

      setErrors(errors);

      if (valid) {
        const res = await fetch("/api/new-user", {
          method: "post",
          body: JSON.stringify(data),
        });

        const body = await res.json();

        if (res.status === 422) {
          setErrors(body);
        } else {
          // Do something on success
        }
      }
    },
    [setErrors]
  );

  return (
    <form onSubmit={onSubmit}>
      <div>
        <label htmlFor="firstName">First Name</label>
        <input id="firstName" name="firstName" onChange={onChange} />
        <div>{errors?.firstName}</div>
      </div>
      <div>
        <label htmlFor="lastName">Last Name</label>
        <input id="lastName" name="lastName" onChange={onChange} />
        <div>{errors?.lastName}</div>
      </div>
      <div>
        <label htmlFor="email">Email</label>
        <input id="email" name="email" onChange={onChange} />
        <div>{errors?.email}</div>
      </div>
      <div>
        <button type="submit">Submit</button>
      </div>
    </form>
  );
}

In the above example, the model rules are validated once and the validation logic is reused on the client and server. This allows for code sharing for full-stack JS apps.

Adding Custom Rules

You can add any custom rule to the validator by defining a function and using the R.test rule:

import { R } from "use-model-validation";

// Custom rule to check if a field matches another field
function match(fields, message) {
  return R.test((data) => {
    return data[fields[0]] === data[fields[1]];
  }, message);
}

const newPassword = createModel({
  rules: {
    password: [R.required("This field is required")],
    passwordConfirmation: [
      R.required("This field is required"),
      match(["password", "passwordConfirmation"], "Passwords don't match"),
    ],
  },
});

Rules

Documentation of built-in rules.

| Rule | Description | Usage | | ---------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | required | Check if any file, string, number, or array value has a size > 0 | R.required("Required") | | min | Check if any file, string, number, or array value has a size >= min | R.min(1, "Too small, must be :min or more") | | max | Check if any file, string, number, or array value has a size <= max | R.min(2, "Too large, must be :max or less") | | between | Check if any file, string, number, or array value has a size between min and max | R.between([1, 2], "Wrong range, must be between :min and :max") | | test | Check if a custom function passes | R.test((data) => data.field === "blah", "Field must be blah") | | format | Check if a value matches a format | R.format(/^[0-9]$/, "Must be a single digit number") | | email | Check if a value is a valid email | R.email("Invalid email") | | mobileUK | Check if a value is a valid UK mobile number | R.mobileUK("Invalid mobile number") | | mobileUS | Check if a value is a valid US mobile number | R.mobileUS("Invalid mobile number") | | postcodeUK | Check if a value is a valid UK postcode | R.postcodeUK("Invalid postcode") | | postcodeUS | Check if a value is a valid US postcode | R.postcodeUS("Invalid postcode") |

Model API

set

Override data on the model, e.g.:

// Delete all fields
model.set({});

// Override firstName field but leave other fields untouched
mode.set((data) => {
  data.firstName = data.firstName.toUpperCase();
  return data;
});

update

Similar to set except fields will not be reset, e.g.:

// Update only the firstName field
model.update({ firstName: "Test" });

// Uppercase all fields
model.update((data) => {
  for (const field in data) {
    data[field].toUpperCase();
  }
  return data;
});

validate

Validate the model and return errors (object), valid (boolean), and data (object), e.g.:

const { errors, valid, data } = model.validate();

if (valid) {
  console.log(data);
} else {
  console.log(errors);
}

fresh

Create a fresh instance of the model, e.g.:

const newModel = model.fresh({ firstName: "New" });

const { firstName: firstName1 } = model.get();
const { firstName: firstName2 } = newModel.get();

// firstName1 will now be different from firstName2

get

Get data stored in the model, e.g.:

model.set({ firstName: "Foo", lastName: "Bar" });
const data = model.get();
// data will now contain: { firstName: "Foo", lastName: "Bar" }

on/off

Add/remove event listener, e.g.:

function onSet(newData) {
  console.log(newData);
}
function onUpdate(newData) {
  console.log(newData);
}
function onValidate(result) {
  console.log(result);
}

// Add/remove listeners
model.on("set", onSet);
model.off("set", onSet);
model.on("update", onUpdate);
model.off("update", onUpdate);
model.on("validate", onValidate);
model.off("validate", onValidate);

Milestones

  • [x] Event emitter per model
  • [ ] Global event emitter for all models (created, updated, error)
  • [ ] Global field rule definitions
  • [ ] Global field error message definitions
  • [x] Better TypeScript support
  • [x] >85% test coverage