agentsclimarketplace

Validation rules

Skill hassanzohdy/mongez-react-form/skills/validation-rules

Use when adding validation to form controls, choosing the right built-in rules, overriding error messages, or writing a custom rule. Covers the full rules list, the InputRule interface, async validation that genuinely gates submission in v4, per-field Standard Schema, per-instance message overrides via errors / errorKeys props, and the validateAll mode.From its SKILL.md

Install
npx -y skills add hassanzohdy/mongez-react-form --skill validation-rules

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

12.4 KB, as published. Nobody here has run it

Validation rules

Apply this skill whenever the user needs to validate input — whether by composing built-in rules or by writing new ones.

How rules work

A rule is plain data: an InputRule object passed in the rules array to useFormControl. Each rule has a validate function that returns either:

  • undefined / null → valid
  • ReactNode → invalid, this is the error message
  • Promise<ReactNode | undefined> → async; other rules block until it resolves

The hook runs rules in array order. The first failing rule short-circuits the rest, unless { validateAll: true } is passed in the second argument.

Rule ordering (v4). The pipeline is built as: the per-instance validate prop first (as a "custom" rule), then the rules array in order, then a per-field schema (if any) appended last. Keep requiredRule first inside your rules array.

Built-in rules

All exported from @mongez/react-form. Each requires a corresponding prop on the input component.

RuleRequired propNotes
requiredRulerequiredEmpty = null / undefined / "" / []
minLengthRuleminLengthStrings and arrays
maxLengthRulemaxLengthStrings and arrays
lengthRulelengthExact length
minRuleminNumeric
maxRulemaxNumeric
emailRuletype="email"
numberRuletype="number"
integerRuletype="integer"
floatRuletype="float"
urlRuletype="url"
patternRulepattern (RegExp)Compiled patterns are cached; source capped at 200 chars. A value over 2000 chars fails the rule (ReDoS guard — not truncated and matched). An oversized or invalid pattern skips validation (fails safe) rather than blocking submit
alphabetRuletype="alphabet"Letters only
matchRulematch (other input name)Must equal that input's value
strongRulestrong (boolean or StrongPasswordCriteria); type="password"Five composable criteria, per-criterion errors via errorsList["strong.<key>"]

A rule with requiresType: "X" is only evaluated when the form control's type matches X. A rule with requiresValue: true (the default) is skipped when the value is empty — that's why requiredRule must be listed before other rules and is the only one with requiresValue: false.

Composing rules in a reusable component

import {
  useFormControl,
  requiredRule,
  minLengthRule,
  maxLengthRule,
  emailRule,
  type FormControlProps,
} from "@mongez/react-form";

export default function TextInput({
  rules = [requiredRule, minLengthRule, maxLengthRule, emailRule],
  ...props
}: FormControlProps) {
  const { value, changeValue, error } = useFormControl({ ...props, rules });
  // ...
}

A consumer activates each rule by passing the corresponding prop. Without the prop, the rule is a no-op.

Overriding error messages per call

Override the whole message

<TextInput
  name="username"
  pattern={/^[a-zA-Z0-9]+$/}
  errors={{
    pattern: "Username must be letters and numbers only",
  }}
/>

errors[ruleName] replaces the message produced by that rule.

Override a placeholder inside the default message

<TextInput
  name="confirmPassword"
  match="password"
  errorKeys={{
    matchingElement: "Password",  // -> "This input is not matching with Password"
  }}
/>

errorKeys is consulted by rules for human-readable substitutions. The hook always populates errorKeys.name from label/placeholder/name and the message's :input placeholder is filled from it. matchRule additionally reads errorKeys.matchingElement to fill the :matchingInput placeholder. Numeric placeholders like :length, :min, :max are filled directly from the input's own props (minLength, min, max) and are not overridable via errorKeys — to change those values, pass a different prop; to change the surrounding text, replace the whole message via errors[ruleName] or override the locale bundle.

Override globally via locale bundles

The package ships translation bundles (enValidationTranslation, arValidationTranslation, etc.) — extend or replace them via @mongez/localization's extend() function. See getting-started.

Writing a custom rule

import { trans } from "@mongez/localization";
import type { InputRule } from "@mongez/react-form";

export const phoneNumberRule: InputRule = {
  name: "phoneNumber",
  requiresType: "phoneNumber",   // only run when type="phoneNumber"
  requiresValue: true,           // skip on empty values
  validate: ({ value }) => {
    if (!/^01[0-2|5]{1}[0-9]{8}$/.test(value)) {
      return trans("validation.phoneNumber");
    }
  },
};

InputRule fields:

{
  name?: string;                        // rule identifier (used in errorsList and errors override)
  validate: (options) => ReactNode | undefined | Promise<...>;
  requiresValue?: boolean;              // default true: skip on empty value
  requiresType?: string;                // run only when formControl.type matches
  preservedProps?: string[];            // props NOT to forward via otherProps
  onInit?: (options) => EventSubscription | undefined;  // setup hook, runs once
}

preservedProps is important when the rule's required prop should NOT leak onto the DOM element (e.g. pattern for patternRule — you don't want <input pattern=/.../> rendered, since the hook handles validation, not the browser).

Async validation (server-side check)

const validateUsername = async ({ value }) => {
  if (!value) return;
  try {
    await api.checkUsernameAvailable(value);
  } catch (err) {
    return err.message;
  }
};

<TextInput name="username" validate={validateUsername} />

When the validator returns a Promise, subsequent rules wait for it to resolve, and the error message is set once the promise settles. Use the validate prop (per-instance) for one-off async checks, or wrap as an InputRule for reusable async logic.

v4: async validation genuinely gates submission

In v4, formControl.validate() returns ReactNode | Promise<ReactNode> and the engine awaits every control in form.validate() before deciding whether to submit. So a pending async rule actually holds back the submit — there's no more race where the form submits before the server check returns.

Three things to know:

  • Sync fast path. If every rule resolves synchronously, validation stays synchronous (no extra microtask) — the common case keeps its original timing. The pipeline only returns a Promise once a rule actually returns one.
  • isValidating. While an async rule is in-flight, formControl.isValidating (and the hook's returned isValidating) is true. Render a spinner from it (see the create-form-control skill).
  • Stale results are discarded. Each validation run captures a monotonic sequence token (validationSeq); if the user types again before an async rule resolves, the newer run wins and the older promise's result is dropped — no flicker of an outdated error.

form.validate() / form.validateVisible() both return Promise<FormControl[]> — await them before reading form.isValid().

Per-field Standard Schema (v4)

Beyond the rules array, a single control can be validated by a Standard Schema (zod / valibot / @warlock.js/seal) via the schema prop or the { schema } option. It is wrapped as an async InputRule (requiresValue: false, so empty values are still handed to the schema and the schema's own optional/required semantics apply) and appended last:

import { v } from "@warlock.js/seal";

<TextInput name="age" schema={v.number().min(18)} />

The first schema issue's message becomes the rendered error. For whole-form schemas and onSubmit type inference, see the standard-schema-validation skill.

validateAll mode

Default: rules run in order, first failure wins, error is the single message.

validateAll: true: every rule runs, every error is captured in errorsList, and error becomes an array of all messages.

const { error, errorsList } = useFormControl(
  { rules: [requiredRule, minLengthRule, patternRule], ...props },
  { validateAll: true }
);
// error is ReactNode[] now

Use when you want to show all violations at once (password strength meters, multi-criteria checklists).

strongRule — composite password validation

strongRule is a meta-rule: one rule that validates five criteria and emits per-criterion errors into formControl.errorsList so callers can render password-strength meters / checklists without composing five separate rules.

Activation

Activated by the strong prop. Requires type="password".

const { value, changeValue, errorsList } = useFormControl({
  ...props,
  rules: [requiredRule, strongRule],
});
<PasswordInput type="password" strong />                          // all defaults
<PasswordInput type="password" strong={{ minLength: 12 }} />      // override one
<PasswordInput type="password" strong={{ symbol: false }} />      // disable one

Default criteria

{
  minLength: 8,   // set to 0 to disable
  uppercase: true,
  lowercase: true,
  digit: true,
  symbol: true,
}

Per-criterion error access

formControl.errorsList contains both the canonical strong entry (whatever the first failing message is, same as every other rule) and namespaced sub-entries:

errorsList["strong"]           // first failing message (consumed by hook → `error`)
errorsList["strong.minLength"] // populated only if length check failed
errorsList["strong.uppercase"] // populated only if uppercase check failed
errorsList["strong.lowercase"]
errorsList["strong.digit"]
errorsList["strong.symbol"]

A criterion that passes does NOT appear in errorsList. Use that to drive a checklist UI:

{["minLength", "uppercase", "lowercase", "digit", "symbol"].map((key) => {
  const failing = Boolean(errorsList[`strong.${key}`]);
  return <li style={{ color: failing ? "red" : "green" }}>{labels[key]}</li>;
})}

Customizing messages

strongRule writes its per-criterion messages directly from the localization bundle (via trans("validation.strongMinLength", ...) etc.) and does NOT consult the per-instance errors prop for individual criteria — errors["strong.minLength"] will not be picked up. To change per-criterion text, override the locale keys globally:

import { extend } from "@mongez/localization";

extend("en", {
  validation: {
    strongMinLength: "Needs at least :length chars",
    strongSymbol: "Add ! @ # $ or similar",
  },
});

Per-instance, you can replace the whole rule's first-message via errors.strong (this hits the generic errors[ruleName] path in the hook, which only sees the first failing message), but there is no per-criterion override at the prop level.

Translation keys (all 6 locales)

validation.strongMinLength (with :length placeholder), validation.strongUppercase, validation.strongLowercase, validation.strongDigit, validation.strongSymbol.

Anti-pattern

Don't combine strongRule with separate minLengthRule — they'll produce duplicate length errors. The minLength criterion inside strongRule already covers length checking for passwords.

onInit hook for rules

InputRule.onInit runs once when the form control mounts. Useful for cross-control rules that need to subscribe to another input's events (e.g. matchRule re-runs when the input it depends on changes). The return value should be an EventSubscription that the hook will unsubscribe on unmount.

Anti-patterns

  • Putting requiredRule after value-dependent rules — they'll never run because they skip on empty values. Always list requiredRule first.
  • Embedding business validation in the input component — pass it via the validate prop or as a reusable InputRule, never hardcode.
  • Returning false from validate — the rule must return a ReactNode (string, JSX, etc.) for the error, not a boolean. false is truthy enough to look like an error but won't render anything.

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.