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 🙏

© 2026 – Pkg Stats / Ryan Hefner

quasar-zod-forms

v0.6.2

Published

Zod-schema-driven Quasar forms with Firestore codecs, JSON form definitions, and a form builder

Readme

quasar-zod-forms

Zod-schema-driven Quasar forms with Firestore codecs. One zod (v4) schema describes the data, the UI, and the storage format:

  • decode (read): Firestore doc → form model (Timestamp → Date, DocumentReference → "collection/id" path)
  • encode (validate + save): form model → Firestore payload — the same direction the backend validates
import { z } from "zod";
import { ZodForm, firestoreTimestamp, firestoreRef, fromFirestore, emptyValues } from "quasar-zod-forms";

const schema = z.object({
  title: z.string().min(3).meta({ label: "Title", section: { title: "Event" } }),
  org: firestoreRef({ label: "Org", options: [{ label: "QCEC", value: "orgs/qcec" }] }),
  startsAt: firestoreTimestamp({ label: "Starts at" }),
  published: z.boolean().default(false).meta({ label: "Published" }),
});

// load
const model = reactive(fromFirestore(schema, (await getDoc(ref)).data()));
// render — @submit receives the ENCODED (storage-shaped) payload
// <ZodForm :schema="schema" :model-value="model" @submit="p => setDoc(ref, p)" />

Point the codecs at the real SDK once per app (defaults are plain-object mocks, which is what the demo uses):

import { Timestamp, doc } from "firebase/firestore";
setFirestoreAdapter({ timestampFromDate: Timestamp.fromDate, docRef: (p) => doc(db, p) });

The same schema file works in Cloud Functions: schema.parse(doc.data()) validates the stored shape; UI metadata is ignored there.

Field metadata (.meta({...}))

label, hint, placeholder, options, props (spread onto the Quasar component), section: { title, caption } (renders a header above the field — content lives in the schema without polluting the data shape), hidden, showIf, optionNotes (same semantics as their JSON-definition counterparts below), and widget to override the auto-mapping:

| zod type | default widget | overrides | |---|---|---| | z.string() | q-input | textarea, color | | z.number() | q-input type=number | slider, rating | | z.boolean() | q-checkbox | toggle | | z.enum() | q-select | radio, btn-toggle | | z.array(enum) | q-select multiple | | | z.array(object) | repeater (add/remove rows) | | | z.object() | nested group | | | z.date() / firestoreTimestamp() | date / datetime picker | |

Text-entry and select widgets (q-input/q-select/q-file/date pickers) render with Quasar's filled design by default — override per field with props: { filled: false, outlined: true } (or any other design prop).

Validation runs in the encode direction (z.safeEncode) on submit, then live; errors are flattened by path and shown per field, including inside nested objects and array rows.

JSON form definitions & builder

Forms can also be defined as serializable JSON (storable in Firestore) and hydrated into a zod schema — same renderer, same validation, same codecs:

import { hydrateForm, FormBuilder } from "quasar-zod-forms";

const schema = hydrateForm({
  fields: [
    { key: "about", type: "section", label: "About", hint: "Display-only header" },
    { key: "name", type: "text", label: "Name", min: 2 },
    { key: "team", type: "select", options: ["Events", "Comms"] },
    { key: "startDate", type: "timestamp", label: "Start date" },
    { key: "attendees", type: "repeater", min: 1, fields: [{ key: "name", type: "text" }] },
  ],
});
// <ZodForm :schema="schema" ... /> as usual

Field type is one of FIELD_TYPES (text, textarea, email, url, number, slider, rating, toggle, checkbox, select, multiselect, combobox, radio, btn-toggle, color, date, datetime, timestamp, ref, upload, section, html, group, repeater), plus label/hint/placeholder/required/default/ options/min/max/fields. An upload field renders a file picker (accept + maxSizeMB client-side limits); on pick, the file goes through the app-provided upload adapter and the field's stored value becomes the returned metadata object ({ path, name, size, contentType, url? }):

import { setUploadAdapter } from "quasar-zod-forms";
setUploadAdapter({
  upload: async (file, field) => {
    const r = ref(storage, `links/${token}/${field.key}/${file.name}`);
    await uploadBytes(r, file);
    return { path: r.fullPath, name: file.name, size: file.size,
             contentType: file.type, url: await getDownloadURL(r) };
  },
});

A section field is display-only (header + caption from label/hint) and holds no value. An html field is also display-only: its html string is injected into the form via v-html (author-trusted content — never render user-supplied markup). In hand-written schemas use z.unknown().optional().meta({ widget: "html", html: "<p>…</p>" }). A combobox is a text input with filterable suggestions from options — freeform text is allowed (built for things like a 200-entry church list). Definitions are validated with jsonFormSchema before hydration (they're untrusted once stored in a DB), and duplicate keys are rejected.

Per-field extras:

  • showIf: { key, equals? } — render (and require, unless required: false) only while the sibling field key equals equals (default true); when the sibling's value is an array (multiselect), while it contains equals — e.g. showIf: { key: "diets", equals: "Other" }. Values typed behind a gate that later closes are kept in the model but pruned before validation and submit.
  • optionNotes: { "Option": "prompt" } (multiselect) — selecting a listed option reveals a required comment box; the value shape becomes { selected: [], notes: {} }.
  • notesKey: "dietNotes" (with optionNotes) — store the notes in that SIBLING key (an { option: note } record) instead of wrapping the value: the field's own value stays a plain string[]. Built for flat storage shapes like Firestore docs with diets: [] + dietNotes: {}. The sibling key is added to the schema automatically (hidden) unless the definition declares it.
  • hidden: true — never rendered; the value stays in the model and survives encode (e.g. admin-only columns).
  • locked: true — the builder UI shows the field disabled with a lock icon: it can be reordered but not edited or deleted. Use it to seed definitions with fields the backend depends on.
  • props: { filled: true, rounded: true, ... } — spread onto the Quasar component, same as meta.props on hand-written schemas.

Hosts that render their own submit button (via the #actions slot) can trigger the same validation ZodForm's submit runs: call formRef.validate() on the component (paints inline errors and arms live re-validation), or use the exported validateModel(schema, model) outside the component — both prune values behind closed showIf gates before encoding.

<FormBuilder :fields="def.fields" /> is the dynamic builder UI — users add, edit, reorder, and nest fields; the array it mutates IS the JSON definition. See the "Form builder" demo tab for the full builder → JSON → live-preview loop.

Demo

npm install && npm run dev

Three tabs: kitchen sink (all widgets), Firestore binding (mock store, watch the raw doc round-trip), nested & arrays. npm test runs the codec round-trip self-checks.