Lokasi ngalangkungan proxy:   [ UP ]  
[Ngawartoskeun bug]   [Panyetelan cookie]                
Skip to content

osameh15/Form-Management

Repository files navigation

nuxt-form-management

License: MIT

A Vuetify-powered drag-and-drop form builder and form renderer module for Nuxt 3 and Nuxt 4 — with a built-in Jalali + Gregorian date picker, custom progress indicator, and full pixel-parity with the DataGenCloud Forms feature it was extracted from. Companion to nuxt-toast-notification, nuxt-confirm-dialog, and nuxt-input-dialog.

  • Three core components: <FormBuilder>, <FormRenderer>, <FormPreview>
  • Plus: <FormList>, <FormLoader>, <FormDatePicker>, <FormProgress>
  • Schema-first: a versioned plain-JSON schema you can store anywhere
  • Storage-agnostic: events return JSON; an optional FormStorageAdapter interface powers the list/loader helpers
  • Vuetify 3 throughout: dialogs, data tables, buttons, animations, focus management — all from the Vuetify library, no hand-rolled CSS framework
  • Jalali + Gregorian: built-in <FormDatePicker> switches calendars on the fly, with no third-party picker dependency
  • TypeScript-first: every public type is exported

Status

v1.0.0 — first stable release. Full DataGenCloud Forms parity on Vuetify 3.9: drag-and-drop builder with auto-generated field names, an interactive preview, Jalali + Gregorian <FormDatePicker>, <FormProgress>, list/loader with confirm-guarded delete, and optional toast / confirm-dialog integration. 120 unit tests passing.

Migrating from v0.x? The persisted JSON schema is 100% compatible — only the runtime requirements changed (Vuetify 3.9 + Vue 3.5 + MDI). See the CHANGELOG.


Requirements

Peer dependencies

Peer Range Required
vue ^3.5.0 yes
vuetify ^3.9.0 yes (3.9+ — earlier 3.x white-screens under Nuxt 4 / unhead v2)
@mdi/font ^7.4.0 yes (icon set)
nuxt-toast-notification ^1.1.0 optional (notify integration)
nuxt-confirm-dialog ^1.1.0 optional (confirm integration)
npm install nuxt-form-management vuetify @mdi/font
npm install -D vite-plugin-vuetify sass
# optional integrations:
npm install nuxt-toast-notification nuxt-confirm-dialog

Setup

nuxt.config.ts — register vite-plugin-vuetify, transpile Vuetify, and load the stylesheets via the css: array (importing vuetify/styles from inside a CSS file does not resolve reliably under Vite — use the array):

import vuetify from 'vite-plugin-vuetify'

export default defineNuxtConfig({
  modules: ['nuxt-form-management'],
  css: ['vuetify/styles', '@mdi/font/css/materialdesignicons.css'],
  build: { transpile: ['vuetify'] },
  vite: {
    ssr: { noExternal: ['vuetify'] },
    plugins: [vuetify({ autoImport: true })],
  },
})

Register Vuetify with a universal plugin (not .client<v-app> renders during SSR and a client-only plugin throws a Vuetify injection error):

// plugins/vuetify.ts   ← universal, NOT vuetify.client.ts
import { createVuetify } from 'vuetify'
import * as components from 'vuetify/components'
import * as directives from 'vuetify/directives'
import { aliases, mdi } from 'vuetify/iconsets/mdi'

export default defineNuxtPlugin((nuxtApp) => {
  nuxtApp.vueApp.use(createVuetify({
    ssr: true,
    components,
    directives,
    icons: { defaultSet: 'mdi', aliases, sets: { mdi } },
    theme: { defaultTheme: 'formDark', themes: { formDark: { dark: true } } },
  }))
})

Wrap your app in <v-app> (e.g. in app.vue) so Vuetify's theme CSS variables are available to the components.

Quick start — designing a form

<script setup lang="ts">
import type { FormSchema } from 'nuxt-form-management'

const { createSchema } = useFormSchema()
const schema = ref<FormSchema>(createSchema({ formName: 'Daily inspection' }))

function onSave(s: FormSchema) {
  // s is plain JSON. Send it wherever you want — REST, GraphQL, IndexedDB...
  return $fetch('/api/forms', { method: 'POST', body: s })
}
</script>

<template>
  <FormBuilder v-model:schema="schema" @save="onSave" />
</template>

Quick start — filling out a form

<script setup lang="ts">
import type { FormSchema, FormValues } from 'nuxt-form-management'

const { data: schema } = await useFetch<FormSchema>('/api/forms/abc')

function onSubmit(values: FormValues) {
  // values is `{ fieldName: typedValue }`. Numbers are numbers, checkboxes
  // are booleans, dates are ISO-style strings — coerced per field type.
  return $fetch('/api/submissions', { method: 'POST', body: { values } })
}
</script>

<template>
  <FormRenderer :schema="schema" @submit="onSubmit" />
</template>

Read-only preview

<FormPreview :schema="schema" :submission="{ values: { operator: 'Alice' } }" />

Standalone date picker

<FormDatePicker> is also usable on its own anywhere in your app:

<script setup lang="ts">
const date = ref('2024-03-20')
</script>

<template>
  <FormDatePicker
    v-model="date"
    calendar="jalali"
    date-mode="single"
    label="Choose a date"
  />
  <!-- date is always Gregorian ISO-8601, regardless of the calendar view -->
</template>

Module options

formManagement: {
  prefix: 'Form',          // <FormBuilder>, <FormRenderer>, ... — change to 'My' → <MyBuilder> etc.
  theme: 'formDark',       // Name of a Vuetify theme registered in createVuetify()
  iconSet: 'mdi',          // Default icon set; pass 'mdi-svg' / 'fa' etc. if you use a different Vuetify icon set
}

Storage adapter

The module never makes network calls. Two ways to persist data:

A — Listen to events

<FormBuilder
  v-model:schema="schema"
  @save="schema => api.saveForm(schema)"
  @preview="schema => track('preview-opened')"
/>

<FormRenderer
  :schema="schema"
  @submit="values => api.saveSubmission({ formId: schema.formId, values })"
  @validation-failure="errors => track('validation-failed')"
/>

B — Register a FormStorageAdapter

This unlocks <FormList> and <FormLoader> and gives you a single contract you implement once.

// plugins/form-management.client.ts
import type { FormStorageAdapter } from 'nuxt-form-management'

const adapter: FormStorageAdapter = {
  async listForms(filter)   { return $fetch('/api/forms',     { query: filter }) },
  async getForm(id)         { return $fetch(`/api/forms/${id}`) },
  async saveForm(schema)    { await $fetch('/api/forms', { method: 'POST', body: schema }) },
  async deleteForm(id)      { await $fetch(`/api/forms/${id}`, { method: 'DELETE' }) },

  async listSubmissions({ formId })  { return $fetch('/api/submissions', { query: { formId } }) },
  async getSubmission(id)            { return $fetch(`/api/submissions/${id}`) },
  async saveSubmission(submission)   { await $fetch('/api/submissions', { method: 'POST', body: submission }) },
  async deleteSubmission(id)         { await $fetch(`/api/submissions/${id}`, { method: 'DELETE' }) },
}

export default defineNuxtPlugin(() => {
  useFormManagement().registerAdapter(adapter)
})

Sibling-package integration

nuxt-toast-notification and nuxt-confirm-dialog are optional peer dependencies. Install them, add both to modules, then bridge them to this package's notify / confirm hooks in one client plugin:

// nuxt.config.ts
modules: [
  'nuxt-form-management',
  'nuxt-toast-notification',
  'nuxt-confirm-dialog',
]
// plugins/form-management.client.ts
export default defineNuxtPlugin(() => {
  const fm = useFormManagement()
  fm.registerAdapter(adapter)

  // useToast() / useConfirmDialog() are auto-imported by the two modules
  const toast = useToast()
  const dialog = useConfirmDialog()

  fm.registerNotify((level, title, message) => {
    toast[level](title, message ?? '')
  })

  fm.registerConfirm(async (opts) => {
    const action = await dialog.show({
      type: opts.type === 'danger' ? 'error' : (opts.type ?? 'warning'),
      title: opts.title,
      message: opts.message ?? '',
      confirmText: opts.confirmLabel,
      cancelText: opts.cancelLabel,
    })
    return action === 'confirm'
  })
})

With this wired, the builder's save validation, <FormList> / submission deletes etc. surface as toasts and confirm dialogs. Without it, the components fall back to console.warn for notify and always-true for confirm.


Schema reference

interface FormSchema {
  schemaVersion: 1
  formId: string
  formName: string
  rows: Array<{
    id: string
    columns: Array<{
      id: string
      colSpan: number      // 1-12; sum of colSpans in a row MUST equal 12
      field: FormField
    }>                     // 1-3 columns per row
  }>
  metadata?: Record<string, unknown>
  createdAt?: string
  updatedAt?: string
}

Field types

type Required props Optional props
text id, name, label, required placeholder
number same unit (e.g. "kg")
select same + options: string[]
checkbox same
textarea same
date same + dateMode, dateType calendar ('gregorian' | 'jalali'), dateColumns, placeholder

name MUST match /^[a-zA-Z_][a-zA-Z0-9_]*$/ and be unique within a form. Renderers use name as the submission's key.

Date values in submissions are always Gregorian ISO-8601 strings (YYYY-MM-DD) regardless of which calendar the user picks dates in. This keeps the persisted schema deterministic.

Forward compatibility

A renderer running an older schemaVersion will:

  • log one console.warn per unknown field type
  • skip rendering the unknown field
  • omit it from the @submit payload

So a v2 schema field can be added without breaking v1 consumers.

Legacy format helpers

For backends that store field metadata as a single colon-separated string (the format DataGenCloud uses):

import { parseLegacy, serializeLegacy } from 'nuxt-form-management/legacy'

const parsed = parseLegacy(
  'rowIndex:0, colIndex:0, colSpan:6, label:Operator, options:[A,B,C]',
  'select',
)

The canonical schema is JSON. Use these only when interfacing with a legacy column.


Component reference

<FormBuilder>

Prop Type Description
schema FormSchema | null v-model-able schema.
isSaving boolean Disables the Save button while in flight.
saveLabel string Override the Save button label.
onValidationFailure (title, message) => void Called on validation-fail save. Falls back to the registered notify handler.
Event Payload When
update:schema FormSchema Any internal change (v-model).
save FormSchema Save button clicked + schema is valid.
preview FormSchema Preview dialog opened.

Dropping a field auto-generates its name as <type>_<counter> (e.g. text_1, select_2) — unique across the form; the label is left blank for the designer to fill (and is required to save). The built-in Preview is interactive — you can type, select and pick dates to test the form exactly as a respondent would.

Slot Scope props Description
filters { schema, update } Inject custom metadata fields above the canvas.

<FormRenderer>

Prop Type Description
schema FormSchema | null The form to render.
initialSubmission FormSubmission | FormValues | null Pre-fills inputs.
readonly boolean Lock all inputs and hide Submit.
hideActions boolean Hide bundled Submit; render your own via the actions slot.
submitLabel string Override the Submit button label.
hideTitle boolean Don't render formName as a heading.
density 'comfortable' | 'default' | 'compact' Forwarded to every Vuetify input.
onValidationFailure (title, message, errors) => void Falls back to notify if not provided.
Event Payload When
update:submission FormValues Any input edit.
submit FormValues Submit clicked + validation passed.
validation-failure SubmissionError[] Submit clicked + validation failed.
Slot Scope props Description
actions { submit, reset, isSubmitting } Replace the bundled Submit button.
date-field { field, modelValue, onUpdate, readonly, hasError } Replace the built-in date picker.
empty Shown when schema has no fields.

<FormPreview>

Read-only sibling of <FormRenderer>. Same schema + submission props, no events, all inputs disabled / readonly. Same date-field and empty slots.

<FormList>

Renders a Vuetify v-data-table of forms returned by the registered adapter. Emits @edit, @load, @delete. Shows a no-adapter empty state when no adapter is registered. Loading state uses v-skeleton-loader rows.

<FormLoader>

A "pick one of my saved forms" widget. filters slot lets you inject custom criteria forwarded to adapter.listForms(). Loading state uses <FormProgress>.

<FormDatePicker>

Self-contained Jalali + Gregorian date picker built on Vuetify.

Prop Type Default
modelValue string | string[] | null null
calendar 'gregorian' | 'jalali' 'gregorian'
dateType 'date' | 'datetime' | 'time' 'date'
dateMode 'single' | 'range' | 'multi' 'single'
label, placeholder string ''
readonly, required, error boolean false
errorMessages string | string[] []
density 'comfortable' | 'default' | 'compact' 'comfortable'
Event Payload
update:modelValue string | string[] | null (Gregorian ISO-8601)

<FormProgress>

Custom modern progress indicator (v-progress-circular + scoped gradient + glow pulse). Use it directly for any loading state in your app that should match the package's look.

Prop Type Default
size number 56
width number 4
color string 'primary'
label string ''

Theme

<FormBuilder> and friends consume Vuetify's theme via useTheme(). Define your themes in createVuetify({ theme: { themes: { ... } } }) and pass the theme name to the module options:

formManagement: {
  theme: 'myDarkTheme',
}

A few scoped CSS variables (drop-zone animations, hover-lift shadows) also read from --v-theme-primary, so they track your accent color automatically.

Persian font (opt-in)

If your app uses Persian script, import the bundled Shabnam font:

// nuxt.config.ts
css: ['nuxt-form-management/fonts/shabnam.css']

Development

npm install
npm run dev:prepare        # build module stubs + prepare playground
npm run dev                # start the playground (Nuxt dev server)
npm run lint
npm run test
npm run test:coverage
npm run prepack            # build dist/

License

MIT — see LICENSE.

About

A zero-dependency drag-and-drop form builder and form renderer module for Nuxt 3 and Nuxt 4 — no Vuetify, no MDI, no third-party CSS framework, no drag-and-drop library. Companion to nuxt-toast-notification, nuxt-confirm-dialog, and nuxt-input-dialog.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors