diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000..6f6350f --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,62 @@ +name: Deploy Docs + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build docs + run: pnpm docs:build + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: docs/.vitepress/dist + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + name: Deploy + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.gitignore b/.gitignore index c592a91..e2e0329 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ node_modules dist -*.log +.DS_Store +docs/.vitepress/dist +docs/.vitepress/cache .DS_Store coverage .vscode diff --git a/DOCS_SETUP.md b/DOCS_SETUP.md new file mode 100644 index 0000000..e03a8fe --- /dev/null +++ b/DOCS_SETUP.md @@ -0,0 +1,217 @@ +# QuickForms Documentation Setup + +✅ **Complete VitePress documentation site created!** + +## What's Been Set Up + +### 1. VitePress Installation & Configuration +- ✅ VitePress and Vue installed as dev dependencies +- ✅ Beautiful purple-themed configuration with custom branding +- ✅ Full-text search enabled +- ✅ Dark mode support +- ✅ Mobile responsive design +- ✅ Automatic sidebar navigation + +### 2. Documentation Structure + +``` +docs/ +├── .vitepress/ +│ ├── config.mts # VitePress configuration +│ └── theme/ +│ ├── index.ts # Theme setup +│ └── custom.css # Custom purple branding +├── guide/ # User guides +│ ├── what-is-quickforms.md ✅ Complete +│ ├── getting-started.md ✅ Complete +│ ├── comparison.md ✅ Complete +│ ├── schema-basics.md ✅ Complete +│ ├── field-types.md ✅ Stub +│ ├── validation.md ✅ Stub +│ ├── complex-types.md 📝 TODO +│ ├── custom-components.md 📝 TODO +│ ├── theming.md 📝 TODO +│ ├── custom-validators.md 📝 TODO +│ ├── i18n.md 📝 TODO +│ └── rbac.md 📝 TODO +├── api/ # API reference +│ ├── form-options.md ✅ Complete +│ ├── components.md 📝 TODO +│ ├── composables.md 📝 TODO +│ ├── schema-extensions.md 📝 TODO +│ └── testers-registry.md 📝 TODO +├── examples/ # Examples +│ ├── basic-form.md ✅ Complete +│ ├── nested-objects.md 📝 TODO +│ ├── arrays.md 📝 TODO +│ ├── conditional-fields.md 📝 TODO +│ ├── custom-validation.md 📝 TODO +│ └── theming.md 📝 TODO +├── packages/ # Package docs +│ ├── core.md 📝 TODO +│ ├── vue.md 📝 TODO +│ └── quasar.md 📝 TODO +├── public/ # Static assets +├── index.md ✅ Homepage complete +└── README.md ✅ Setup instructions +``` + +### 3. Scripts Added to package.json + +```json +{ + "docs:dev": "vitepress dev docs", // Start dev server + "docs:build": "vitepress build docs", // Build for production + "docs:preview": "vitepress preview docs" // Preview production build +} +``` + +### 4. GitHub Actions Deployment +- ✅ Workflow file created: `.github/workflows/deploy-docs.yml` +- ✅ Automatically deploys to GitHub Pages on push to main +- ✅ Manual deployment trigger available + +### 5. Content Migrated from README + +The following content has been extracted and organized from your main README: + +- **Homepage** - Hero section with features and quick example +- **What is QuickForms** - Philosophy and use cases +- **Getting Started** - Installation and first form tutorial +- **Comparison** - Detailed comparison with JSONForms +- **Schema Basics** - JSON Schema fundamentals +- **Form Options API** - Complete API reference +- **Basic Example** - Working code example with explanations + +## Usage + +### Development + +```bash +# Start local dev server (with hot reload) +pnpm docs:dev + +# Opens at http://localhost:5175 (or next available port) +``` + +### Build & Preview + +```bash +# Build for production +pnpm docs:build + +# Preview production build +pnpm docs:preview +``` + +### Deploy to GitHub Pages + +1. **Enable GitHub Pages** in your repository settings: + - Go to Settings → Pages + - Source: GitHub Actions + +2. **Push to main branch** - The workflow will automatically build and deploy + +3. **Your docs will be live at:** + - `https://.github.io//` + - Example: `https://quickflo.github.io/quickforms/` + +## Next Steps + +### Content to Add + +The following pages are stubbed out or need to be created: + +#### High Priority +1. **`guide/complex-types.md`** - Nested objects, arrays, oneOf/anyOf/allOf +2. **`guide/custom-validators.md`** - Sync/async validation examples +3. **`guide/theming.md`** - CSS variables and styling guide +4. **`api/schema-extensions.md`** - Document all `x-*` attributes + +#### Medium Priority +5. **`api/components.md`** - DynamicForm, field components reference +6. **`api/composables.md`** - useFormField, useFormContext docs +7. **`examples/nested-objects.md`** - Working example +8. **`examples/arrays.md`** - Working example +9. **`examples/conditional-fields.md`** - oneOf/anyOf examples + +#### Lower Priority +10. **`guide/custom-components.md`** - Component registry and testers +11. **`guide/i18n.md`** - Internationalization guide +12. **`guide/rbac.md`** - Role-based access control +13. **`api/testers-registry.md`** - Tester system reference +14. **Package docs** - core.md, vue.md, quasar.md + +### Extracting Content from README + +Your main README is quite comprehensive. Consider extracting these sections: + +- **Validation section** → `guide/validation.md` (partially done) +- **Complex types examples** → `guide/complex-types.md` +- **Custom validators section** → `guide/custom-validators.md` +- **Theming section** → `guide/theming.md` +- **RBAC section** → `guide/rbac.md` +- **i18n section** → `guide/i18n.md` +- **Supported JSON Schema features** → `api/schema-extensions.md` + +### Updating the Main README + +Once docs are complete, simplify the main README to: +- Brief introduction +- Quick install and example +- Link to full documentation +- Contributing guidelines +- License + +## Features + +### What Works Out of the Box + +- ✅ **Beautiful UI** - Purple-themed, modern design +- ✅ **Search** - Full-text search across all docs +- ✅ **Code highlighting** - Syntax highlighting for Vue, TypeScript, etc. +- ✅ **Code groups** - Tab-based code examples (pnpm/npm/yarn) +- ✅ **Navigation** - Automatic sidebar and page navigation +- ✅ **Mobile responsive** - Works great on all devices +- ✅ **Dark mode** - Automatic theme switching +- ✅ **Fast** - Built with Vite, instant HMR + +### Customization Options + +The theme is already customized with QuickForms branding: +- Purple color scheme (`#8b5cf6`) +- Custom hero gradient +- QuickForms logo support (add `docs/public/logo.svg`) + +To further customize, edit: +- `docs/.vitepress/config.mts` - Site configuration +- `docs/.vitepress/theme/custom.css` - Styling + +## Assets + +Add the following assets to `docs/public/`: +- `logo.svg` - QuickForms logo for navigation +- Copy `docs/assets/banner.readme.1280x320.png` to `docs/public/assets/` for homepage + +## Tips + +1. **Keep README concise** - Now that you have docs, the README can be much shorter +2. **Link to docs** - Add a prominent "Documentation" link in README +3. **Use VitePress features**: + - `::: warning` / `::: tip` / `::: danger` callouts + - Code groups for multi-language examples + - Custom components in markdown +4. **Test examples** - Make sure all code examples actually work +5. **Internal linking** - Use relative paths: `[Link](/guide/page)` + +## Maintenance + +- Update VitePress: `pnpm add -D vitepress@latest` +- Check for broken links: VitePress has built-in dead link checking +- Review analytics: Consider adding Google Analytics or similar + +--- + +**Your docs are ready to go! 🚀** + +Run `pnpm docs:dev` to see them in action. diff --git a/README.md b/README.md index 7f99add..b2a31f5 100644 --- a/README.md +++ b/README.md @@ -2,103 +2,39 @@ # QuickForms -**A Vue 3 JSON Schema form generator, with reasonable escape hatches.** +**Vue 3 JSON Schema forms with sensible defaults and reasonable escape hatches.** -QuickForms generates forms from JSON Schema with sensible defaults and clear customization paths. Built for Vue 3 with a framework-agnostic core. +[![Documentation](https://img.shields.io/badge/docs-quickforms-blue)](https://quickforms.dev) +[![npm version](https://img.shields.io/npm/v/@quickflo/quickforms.svg)](https://www.npmjs.com/package/@quickflo/quickforms) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -## Table of Contents +## ✨ Features -- [Motivation](#motivation) -- [Comparison with JSONForms](#comparison-with-jsonforms) -- [Features](#features) -- [Installation](#installation) -- [Quick Start](#quick-start) -- [Usage Guide](#usage-guide) - - [Validation](#validation) - - [Complex Types](#complex-types) - - [Custom Validators](#custom-validators) - - [Internationalization & Customization](#internationalization--customization) - - [Role-Based Access Control](#role-based-access-control) -- [Custom Components](#custom-components) -- [Theming](#theming) -- [Form Options API](#form-options-api) -- [Architecture](#architecture) -- [Supported JSON Schema Features](#supported-json-schema-features) - - [Custom Extensions (x-* attributes)](#custom-extensions-x--attributes) -- [Roadmap](#roadmap) -- [FAQ](#faq) -- [Contributing](#contributing) -- [Packages](#packages) -- [License](#license) +- 🚀 **Fast & Lightweight** - ~56KB gzipped, framework-agnostic core +- 🎨 **Themeable** - 60+ CSS variables, no design system lock-in +- 🔧 **Escape Hatches** - Override defaults without rebuilding components +- 📝 **JSON Schema** - Full Draft 7+ support (oneOf, anyOf, nested objects, arrays) +- ✅ **Flexible Validation** - Three modes, custom sync/async validators +- 🔐 **Role-Based Access** - Field-level visibility and editability control +- 🌍 **i18n Ready** - Customize all labels and messages +- 🧩 **Extensible** - Custom component registry with tester system +- 💜 **Quasar Support** - Pre-styled components out of the box -## Motivation +## 📸 Screenshots -JSON Schema form libraries are powerful but often rigid. QuickForms provides escape hatches at common pain points: +### Quasar +![QuickForms with Quasar](./docs/assets/quickforms-showcase-quasar.png) -- Don't like the default placeholder? Override it globally or per-field -- Need custom validation? Add sync/async validators alongside JSON Schema rules -- Enum values too technical? Map them to friendly labels with `x-enum-labels` -- Want dynamic hints? Use `hintRenderer` for full control +### Plain Vue +![QuickForms with Plain Vue](./docs/assets/quickforms-showcase-vue.png) -Sensible defaults, clear customization paths. No rebuilding components. - -## Comparison with JSONForms - -| Feature | QuickForms | JSONForms | -|---------|------------|------------| -| **UI Schema** | Optional | Required for layouts | -| **Theming** | CSS variables | Framework-dependent | -| **Custom Validators** | Built-in (sync + async) | Not built-in | -| **Vue Version** | Vue 3 Composition API | Vue 2/3 | -| **Bundle Size** | ~56KB gzipped | Varies by renderers | -| **Status** | Early Stage | Mature | - -**Note**: This comparison reflects JSONForms as of late 2024. QuickForms is newer and less battle-tested. - -## Features - -### Core -- Framework-agnostic core with Vue 3 bindings -- Full TypeScript support -- CSS variable theming (60+ variables) -- Extensible component registry - -### Validation -- JSON Schema validation (via Ajv) -- Three validation modes: show errors, hide errors, no validation -- Custom sync and async validators -- Configurable debouncing for async validation -- Cross-field validation support -- Custom error messages (per-field or global) - -### Field Types -- Primitives: string, number, integer, boolean -- Formats: email, url, password, textarea, date, time, date-time -- Complex: nested objects, arrays, enums -- Conditional: oneOf, anyOf, allOf - -### Customization -- Custom display labels for enum values (`x-enum-labels`) -- Custom array item labels with templates (`x-item-label`) -- Role-based field visibility (`x-roles`) -- HTML hints with dynamic rendering (`x-hint`, `hintRenderer`) -- Hint visibility control (always/focus/hover) -- Autocomplete for large enum lists -- Internationalization support - -## Installation +## 📦 Installation ```bash -# Install core and vue package pnpm add @quickflo/quickforms @quickflo/quickforms-vue @quickflo/quickforms-quasar - -# Peer dependencies -pnpm add vue vee-validate ``` -## Quick Start - -### Basic Example +## 🚀 Quick Start ```vue -``` - -## Usage Guide - -### Validation - -#### Validation Modes - -Control when and how validation errors are displayed: - -```vue - -``` - -- **`ValidateAndShow`** (default): Validates as you type and shows errors -- **`ValidateAndHide`**: Validates silently, errors hidden from user but form won't submit if invalid -- **`NoValidation`**: Completely disables validation - -#### Custom Error Messages - -Two ways to customize validation messages: - -**1. In Schema (recommended for reusable schemas):** - -```typescript -const schema: JSONSchema = { - type: 'object', - properties: { - password: { - type: 'string', - minLength: 8, - 'x-error-messages': { - required: 'Password is required for security', - minLength: 'Password must be at least 8 characters for your security' - } - } - }, - required: ['password'] -}; -``` - -**2. In Form Options (for app-specific messages):** - -```vue - -``` - -#### Validation Events - -React to validation state changes: - -```vue - - - -``` - -#### Required Fields - -Fields are marked as required using the `required` array in the parent object schema: - -```typescript -const schema: JSONSchema = { - type: 'object', - properties: { - name: { type: 'string', title: 'Name' }, - email: { type: 'string', format: 'email', title: 'Email' }, - age: { type: 'number', title: 'Age' } - }, - required: ['name', 'email'] // name and email are required, age is optional -}; -``` - -**Visual indicators:** -- Required fields display an asterisk (`*`) next to the label -- Attempting to submit with missing required fields shows validation errors -- With `ValidateAndShow` mode, errors appear as you type -- With `ValidateAndHide` mode, errors are suppressed until form submission - -#### Form Submission - -QuickForms prevents invalid form submission based on: -1. **Required fields** - All fields in the `required` array must have values -2. **Format validation** - Fields with `format` (e.g., `email`, `url`, `date`) must match the expected format -3. **Constraint validation** - Fields must satisfy constraints like `minLength`, `maxLength`, `minimum`, `maximum`, `pattern`, etc. -4. **Custom validators** - Any custom validators defined in `options.validators` must pass - -```vue - - - ``` -**Submission behavior:** -- **`ValidateAndShow`** mode: Submit button is always enabled, clicking shows all validation errors if invalid -- **`ValidateAndHide`** mode: Submit button is always enabled, but submission is blocked if invalid (no errors shown) -- **`NoValidation`** mode: Submit always succeeds, no validation performed -- The `@submit` event only fires when the form is valid (or validation is disabled) -- You can disable the default submit button and handle submission programmatically using the validation events - -### Complex Types - -#### Nested Objects - -```typescript -const schema: JSONSchema = { - type: 'object', - properties: { - address: { - type: 'object', - title: 'Address', - properties: { - street: { type: 'string', title: 'Street' }, - city: { type: 'string', title: 'City' }, - zip: { type: 'string', pattern: '^\\d{5}$' } - }, - required: ['street', 'city', 'zip'] - } - } -}; -``` - -#### Arrays - -```typescript -const schema: JSONSchema = { - type: 'object', - properties: { - hobbies: { - type: 'array', - title: 'Hobbies', - minItems: 2, - items: { - type: 'string', - title: 'Hobby' - } - } - } -}; -``` - -#### Arrays with Custom Labels - -Use `x-item-label` to customize how array items are displayed: - -```typescript -const schema: JSONSchema = { - type: 'object', - properties: { - workHistory: { - type: 'array', - title: 'Work History', - 'x-item-label': '{{company}} - {{position}}', // Template interpolation - items: { - type: 'object', - properties: { - company: { type: 'string', title: 'Company' }, - position: { type: 'string', title: 'Position' }, - years: { type: 'number', title: 'Years' } - } - } - } - } -}; -``` - -Set to `"none"` or `false` to hide labels entirely. - -#### Enum Fields with Custom Labels - -Use `x-enum-labels` to provide custom display text for enum options while keeping the underlying value: - -```typescript -const schema: JSONSchema = { - type: 'object', - properties: { - status: { - type: 'string', - enum: ['draft', 'active', 'paused', 'archived'], - title: 'Status', - // Custom display labels (value -> label mapping) - 'x-enum-labels': { - 'draft': '📝 Draft', - 'active': '✅ Active', - 'paused': '⏸️ Paused', - 'archived': '📦 Archived' - } - }, - httpMethod: { - type: 'string', - enum: ['GET', 'POST', 'PUT', 'DELETE'], - title: 'HTTP Method', - 'x-enum-labels': { - 'GET': 'GET - Retrieve data', - 'POST': 'POST - Create resource', - 'PUT': 'PUT - Update resource', - 'DELETE': 'DELETE - Remove resource' - } - } - } -}; -``` - -#### Enum Autocomplete - -For enum fields with many options, enable autocomplete using HTML5 datalist: - -```typescript -// Option 1: Enable for a specific field -const schema: JSONSchema = { - type: 'object', - properties: { - country: { - type: 'string', - enum: ['US', 'CA', 'UK', 'FR', /* ...100 countries */], - 'x-component-props': { - autocomplete: true // Enable datalist for this field - } - } - } -}; - -// Option 2: Enable globally with threshold -const options = { - componentDefaults: { - select: { - autocomplete: false, - autocompleteThreshold: 5 // Auto-enable for 5+ options - } - } -}; -``` - -#### Conditional Schemas (oneOf) - -```typescript -const schema: JSONSchema = { - type: 'object', - properties: { - paymentMethod: { - type: 'object', - title: 'Payment Method', - oneOf: [ - { - title: 'Credit Card', - properties: { - type: { const: 'credit_card' }, - cardNumber: { type: 'string', pattern: '^\\d{16}$' }, - cvv: { type: 'string', pattern: '^\\d{3}$' } - }, - required: ['cardNumber', 'cvv'] - }, - { - title: 'PayPal', - properties: { - type: { const: 'paypal' }, - email: { type: 'string', format: 'email' } - }, - required: ['email'] - } - ] - } - } -}; -``` - -### Custom Validators - -Add custom validation logic beyond JSON Schema capabilities. Supports both sync and async validators seamlessly. - -#### Sync Validators (Cross-Field Validation) - -```vue - - - -``` - -#### Async Validators (API Validation) - -```vue - - - -``` - -#### Validator Return Types - -Validators can return: -- `true` - Valid -- `false` - Invalid (generic error message) -- `string` - Invalid with custom error message -- `{ valid: boolean, message?: string }` - Object format -- `Promise` - Async validation - -#### Business Logic Example - -```typescript -validators: { - // Age must match birthdate - age: (value, allValues) => { - if (!allValues.birthdate) return true; - - const birthYear = new Date(allValues.birthdate).getFullYear(); - const calculatedAge = new Date().getFullYear() - birthYear; - - if (Math.abs(value - calculatedAge) > 1) { - return 'Age doesn\'t match birth date'; - } - return true; - }, - - // Conditional required - otherSpecify: (value, allValues) => { - if (allValues.category === 'other' && !value) { - return 'Please specify when selecting "Other"'; - } - return true; - } -} -``` - -#### Global vs Per-Field Debouncing - -```typescript -// Apply same debounce to all async validators -validatorDebounce: 300 - -// Or configure per field -validatorDebounce: { - username: 500, // Slower API - email: 300, // Faster API - zipCode: 1000 // Very slow API -} -``` - -### Internationalization & Customization - -#### Customizable Labels - -Customize all UI text for internationalization or branding: - -```vue - - - -``` - -**Available Labels:** - -```typescript -interface FormLabels { - selectPlaceholder?: string; // Default: "Select an option..." - addItem?: string; // Default: "Add item" - removeItem?: string; // Default: "Remove" - submit?: string; // Default: "Submit" - showPassword?: string; // Default: "Show password" - hidePassword?: string; // Default: "Hide password" -} -``` - -#### Component Defaults - -Configure default behavior for all components of a given type: - -```typescript -const options = { - componentDefaults: { - select: { - autocomplete: true, // Enable autocomplete for all selects - autocompleteThreshold: 10 // Or only when 10+ options - }, - array: { - collapsible: true, // Allow collapsing array items - defaultCollapsed: false // Start expanded - }, - number: { - prefix: '$', // Prefix for number display - suffix: '%' // Suffix for number display - } - } -}; -``` - -**Field-Level Overrides:** - -Use `x-component-props` in schema to override component defaults for specific fields: - -```typescript -const schema: JSONSchema = { - type: 'object', - properties: { - country: { - type: 'string', - enum: ['US', 'CA', 'UK', /* ...many options */], - 'x-component-props': { - autocomplete: true // Override for this field only - } - } - } -}; -``` - -### Role-Based Access Control - -Control field visibility and editability based on user roles: - -```typescript -const schema: JSONSchema = { - type: 'object', - properties: { - systemId: { - type: 'string', - title: 'System ID', - 'x-hidden': true // Always hidden - }, - adminOnlyField: { - type: 'string', - title: 'Admin Only', - 'x-roles': { - admin: ['view', 'edit'], - user: [], - guest: [] - } - }, - readOnlyForUsers: { - type: 'string', - title: 'Read-Only for Users', - 'x-roles': { - admin: ['view', 'edit'], - user: ['view'], - guest: [] - } - } - } -}; -``` - -```vue - -``` - -## Custom Components - -QuickForms allows you to register your own components for specific fields using a powerful "tester" system (all credit goes to jsonforms for this design concept). - -```typescript -import { createDefaultRegistry, rankWith, isStringType, and, hasFormat } from '@quickflo/quickforms-vue'; -import MyCustomPhoneInput from './MyCustomPhoneInput.vue'; - -// 1. Create a registry (start with defaults) -const registry = createDefaultRegistry(); - -// 2. Register your component with a tester -// This will match any string field with format: "phone" -registry.register('custom-phone', MyCustomPhoneInput, (schema) => - rankWith(10, and(isStringType, hasFormat('phone'))(schema)) -); - -// 3. Pass registry to the form -// -``` - -### Creating a Custom Component - -Custom components receive standard props: - -```vue - - - -``` - -## Theming - -QuickForms uses CSS custom properties (variables) for styling. You can override these globally or for specific forms. - -```css -/* Global Override */ -:root { - /* Brand Colors */ - --quickform-color-primary: #8b5cf6; /* Purple */ - --quickform-color-error: #ef4444; - - /* Spacing & Radius */ - --quickform-radius-md: 0.75rem; - --quickform-spacing-md: 1rem; - - /* Typography */ - --quickform-font-family: 'Inter', sans-serif; -} - -/* Dark Mode Example */ -.dark-theme { - --quickform-color-bg: #1f2937; - --quickform-color-text: #f3f4f6; - --quickform-color-border: #374151; -} -``` - -See `packages/vue/src/styles/variables.css` for the full list of 60+ customizable variables. - -See **[STYLING_GUIDE.MD](./STYLING_GUIDE.MD)** for a full guide on styling. - -## Form Options API - -Complete reference for the `options` prop: - -```typescript -interface FormOptions { - /** Populate form with default values from schema */ - useDefaults?: boolean; // default: true - - /** Make entire form read-only */ - readonly?: boolean; - - /** Disable entire form */ - disabled?: boolean; - - /** Validation behavior */ - validationMode?: 'ValidateAndShow' | 'ValidateAndHide' | 'NoValidation'; - - /** Custom error messages by field path and rule */ - errorMessages?: Record>; - - /** Custom field validators (sync or async) */ - validators?: Record; - - /** Debounce delay for async validators in milliseconds */ - validatorDebounce?: number | Record; - - /** Application context (user info, roles, etc.) */ - context?: Record; - - /** Custom component registry */ - registry?: ComponentRegistry; - - /** Customizable labels for i18n or branding */ - labels?: FormLabels; - - /** Component-specific default configurations */ - componentDefaults?: ComponentDefaults; -} - -interface FormLabels { - selectPlaceholder?: string; // Default: "Select an option..." - addItem?: string; // Default: "Add item" - removeItem?: string; // Default: "Remove" - submit?: string; // Default: "Submit" - showPassword?: string; // Default: "Show password" - hidePassword?: string; // Default: "Hide password" -} - -interface ComponentDefaults { - select?: { - autocomplete?: boolean; // Default: false - autocompleteThreshold?: number; // Default: 5 - }; - array?: { - collapsible?: boolean; // Default: false - defaultCollapsed?: boolean; // Default: false - }; - number?: { - prefix?: string; // Default: undefined - suffix?: string; // Default: undefined - }; -} - -type ValidatorFunction = ( - value: any, - allValues: Record, - context?: Record -) => boolean | string | Promise; -``` - -## Architecture - -The project is structured as a monorepo: - -- **`@quickflo/quickforms`**: Framework-agnostic logic (validation, schema utils, registry). Can be used to build bindings for React, Angular, etc. -- **`@quickflo/quickforms-vue`**: Vue 3 bindings using Composition API and VeeValidate. -- **`@quickflo/forms-quasar`**: Pre-configured bindings for Quasar framework. - -## Supported JSON Schema Features - -### Types -- `string`, `number`, `integer`, `boolean`, `object`, `array`, `null` - -### String Formats - -QuickForms supports both standard JSON Schema formats and custom format extensions for better UI rendering: - -#### Standard JSON Schema Formats (with validation) -- **`email`** - Renders email input, validates email format (RFC 5321) -- **`url`** / **`uri`** - Renders URL input, validates URI format (RFC 3986) -- **`date`** - Renders date picker, validates ISO 8601 date (YYYY-MM-DD) -- **`time`** - Renders time picker, validates ISO 8601 time (HH:mm:ss or HH:mm:ss.sss) -- **`date-time`** - Renders date+time picker, validates ISO 8601 datetime - -#### Custom Format Extensions (UI hints only, no validation) -- **`password`** - Renders password input with show/hide toggle -- **`textarea`** - Renders multi-line textarea instead of single-line input - -**Note**: The `password` and `textarea` formats are UI hints only and do not perform any validation. They always pass validation regardless of content. Fields with `maxLength > 200` automatically render as textareas. - -**Example**: -```typescript -const schema: JSONSchema = { - type: 'object', - properties: { - email: { - type: 'string', - format: 'email', // Validates email format - title: 'Email Address' - }, - website: { - type: 'string', - format: 'url', // Validates URL format - title: 'Website' - }, - password: { - type: 'string', - format: 'password', // UI hint only - shows password field with toggle - minLength: 8 // Validation still works via other keywords - }, - bio: { - type: 'string', - format: 'textarea', // UI hint only - shows multi-line textarea - maxLength: 500 - } - } -}; -``` - -### Validation Keywords -- **String**: `minLength`, `maxLength`, `pattern`, `format` -- **Number**: `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf` -- **Array**: `minItems`, `maxItems`, `uniqueItems` -- **Object**: `properties`, `required`, nested validation -- **General**: `enum`, `const`, `default` - -### Advanced -- `oneOf`, `anyOf`, `allOf` - Conditional schemas -- Nested objects and arrays (unlimited depth) -- `$ref` and `$defs` for schema references - -### Custom Extensions (`x-*` attributes) - -QuickForms extends JSON Schema with custom `x-*` attributes to provide escape hatches for common customization needs: - -#### `x-hidden` -**Purpose**: Completely hide a field from rendering -**Type**: `boolean` -**Example**: -```json -{ - "systemId": { - "type": "string", - "x-hidden": true - } -} -``` - -#### `x-roles` -**Purpose**: Role-based access control for field visibility and editability -**Type**: `Record` -**Example**: -```json -{ - "adminField": { - "type": "string", - "x-roles": { - "admin": ["view", "edit"], - "user": ["view"], - "guest": [] - } - } -} -``` - -#### `x-item-label` -**Purpose**: Custom labels for array items with template interpolation -**Type**: `string | "none" | false` -**Example**: -```json -{ - "workHistory": { - "type": "array", - "x-item-label": "{{company}} - {{position}}", - "items": { - "type": "object", - "properties": { - "company": { "type": "string" }, - "position": { "type": "string" } - } - } - } -} -``` -Use `"none"` or `false` to hide labels entirely. - -#### `x-enum-labels` -**Purpose**: Custom display text for enum options (value → label mapping) -**Type**: `Record` -**Example**: -```json -{ - "status": { - "type": "string", - "enum": ["draft", "active", "paused"], - "x-enum-labels": { - "draft": "📝 Draft", - "active": "✅ Active", - "paused": "⏸️ Paused" - } - } -} -``` - -#### `x-error-messages` -**Purpose**: Custom validation error messages per rule type -**Type**: `Record` -**Example**: -```json -{ - "password": { - "type": "string", - "minLength": 8, - "x-error-messages": { - "required": "Password is required for security", - "minLength": "Password must be at least 8 characters" - } - } -} -``` - -#### `x-component-props` -**Purpose**: Override component-specific behavior for a single field -**Type**: `Record` -**Example**: -```json -{ - "country": { - "type": "string", - "enum": ["US", "CA", "UK", "..."], - "x-component-props": { - "autocomplete": true - } - } -} -``` - -#### `x-hint` -**Purpose**: HTML-enabled hint text (takes precedence over `description`) -**Type**: `string` -**Example**: -```json -{ - "email": { - "type": "string", - "x-hint": "Read our privacy policy" - } -} -``` - -#### `x-hint-mode` -**Purpose**: Control when hints are displayed -**Type**: `"always" | "focus" | "hover"` -**Default**: `"always"` -**Example**: -```json -{ - "password": { - "type": "string", - "description": "Must be 8+ characters", - "x-hint-mode": "focus" - } -} -``` - ---- - -**Note**: All `x-*` attributes are optional and designed as escape hatches. QuickForms works perfectly with standard JSON Schema—use extensions only when you need them. - -## Roadmap - -- [x] Phase 1: Core Architecture & Validation -- [x] Phase 2: Vue Basic Fields & Theming -- [x] Phase 3: Extensibility & Custom Registries -- [x] Phase 4: Complex Types (Nested Objects, Arrays, OneOf/AnyOf/AllOf) -- [x] Phase 5: Validation System (Modes, Custom Messages, Events) -- [x] Phase 6: Role-Based Access Control -- [x] Phase 7: i18n/Localization -- [x] Phase 8: Quasar Support -- [ ] Phase 9: UI Schema Support (Layouts, Rules) - -## FAQ - -**Q: How is this different from JSONForms?** - -A: QuickForms focuses on modern architecture (Vue 3 Composition API, framework-agnostic core) and developer experience. Key differences: -- Simpler API - no separate UI schema required for basic use -- Built-in role-based access control -- CSS variable theming instead of Material-UI dependency -- Cleaner validation with multiple modes -- Smaller bundle size - -**Q: Can I use this with React/Angular?** - -A: The core is framework-agnostic. Vue bindings are production-ready. React/Angular bindings can be built using the same core. +## 📚 Documentation -**Q: Can I use custom components?** +**[View Full Documentation →](https://quickflo.github.io/quickforms)** -A: Yes! See the "Custom Components" section above. The tester system gives you full control over component selection. +The documentation includes: +- 🚀 **Getting Started** - Installation and basic usage +- 📖 **API Reference** - Complete API for form options, composables, and components +- 💡 **Examples** - Nested objects, arrays, conditional fields, validation, theming, and more +- 🎨 **Theming Guide** - Customize with CSS variables +- 🧩 **Custom Components** - Build and register your own field components -## Contributing +## 🏗️ Architecture -Contributions are welcome! Please read our contributing guidelines and submit PRs to the `main` branch. +QuickForms is built as a monorepo with three packages: -## Packages +- **`@quickflo/quickforms`** - Framework-agnostic core (validation, schema utils, registry) +- **`@quickflo/quickforms-vue`** - Vue 3 bindings using Composition API +- **`@quickflo/quickforms-quasar`** - Pre-configured Quasar components -- **[@quickflo/quickforms](./packages/core)** - Framework-agnostic core -- **[@quickflo/quickforms-vue](./packages/vue)** - Vue 3 bindings +The framework-agnostic core makes it possible to build React or Angular bindings in the future. -## License +## 📄 License MIT diff --git a/STYLING_GUIDE.md b/STYLING_GUIDE.md index 4b4c48a..4683cc9 100644 --- a/STYLING_GUIDE.md +++ b/STYLING_GUIDE.md @@ -241,7 +241,6 @@ registry.register('phone', PhoneInput, (schema) => - `.quickform-oneof-field` - OneOf selectors ### Buttons -- `.quickform-submit` - Submit button - `.quickform-btn` - Generic button - `.quickform-btn-icon` - Icon buttons (array add/remove) - `.quickform-password-toggle` - Password visibility toggle diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts new file mode 100644 index 0000000..7b0180e --- /dev/null +++ b/docs/.vitepress/config.mts @@ -0,0 +1,94 @@ +import { defineConfig } from "vitepress"; + +export default defineConfig({ + title: "QuickForms", + description: + "Vue 3 JSON Schema form generator with reasonable escape hatches", + + ignoreDeadLinks: true, + + themeConfig: { + logo: "/logo.svg", + + nav: [{ text: "Guide", link: "/guide/getting-started" }], + + sidebar: { + "/guide/": [ + { + text: "Introduction", + items: [ + { text: "What is QuickForms?", link: "/guide/what-is-quickforms" }, + { text: "Getting Started", link: "/guide/getting-started" }, + { text: "Comparison", link: "/guide/comparison" }, + ], + }, + { + text: "Essentials", + items: [ + { text: "Schema Basics", link: "/guide/schema-basics" }, + { text: "Field Types", link: "/guide/field-types" }, + { text: "Validation", link: "/guide/validation" }, + { text: "Complex Types", link: "/guide/complex-types" }, + ], + }, + { + text: "Packages", + items: [ + { text: "Vue", link: "/guide/vue" }, + { text: "Quasar", link: "/guide/quasar" }, + ], + }, + { + text: "Advanced", + items: [ + { text: "Custom Components", link: "/guide/custom-components" }, + { text: "Theming", link: "/guide/theming" }, + { text: "Custom Validators", link: "/guide/custom-validators" }, + { text: "Internationalization", link: "/guide/i18n" }, + { text: "Role-Based Access", link: "/guide/rbac" }, + ], + }, + { + text: "API Reference", + items: [ + { text: "Form Options", link: "/guide/form-options" }, + { text: "Schema Extensions", link: "/guide/schema-extensions" }, + { text: "Components", link: "/guide/components" }, + { text: "Composables", link: "/guide/composables" }, + { text: "Testers & Registry", link: "/guide/testers-registry" }, + ], + }, + { + text: "Examples", + items: [ + { text: "Basic Form", link: "/guide/examples/basic-form" }, + { text: "Nested Objects", link: "/guide/examples/nested-objects" }, + { text: "Arrays", link: "/guide/examples/arrays" }, + { + text: "Conditional Fields", + link: "/guide/examples/conditional-fields", + }, + { + text: "Custom Validation", + link: "/guide/examples/custom-validation", + }, + { text: "Theming", link: "/guide/examples/theming" }, + ], + }, + ], + }, + + socialLinks: [ + { icon: "github", link: "https://github.com/quickflo/quickforms" }, + ], + + search: { + provider: "local", + }, + + footer: { + message: "Released under the MIT License.", + copyright: "Copyright © 2024-present QuickForms", + }, + }, +}); diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css new file mode 100644 index 0000000..0ecb700 --- /dev/null +++ b/docs/.vitepress/theme/custom.css @@ -0,0 +1,52 @@ +/** + * Customize default theme styling by overriding CSS variables: + * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css + */ + +:root { + --vp-c-brand-1: #8b5cf6; + --vp-c-brand-2: #7c3aed; + --vp-c-brand-3: #6d28d9; +} + +.dark { + --vp-c-brand-1: #a78bfa; + --vp-c-brand-2: #8b5cf6; + --vp-c-brand-3: #7c3aed; +} + +/** + * Component: Button + * -------------------------------------------------------------------------- */ + +:root { + --vp-button-brand-border: transparent; + --vp-button-brand-text: var(--vp-c-white); + --vp-button-brand-bg: var(--vp-c-brand-3); + --vp-button-brand-hover-border: transparent; + --vp-button-brand-hover-text: var(--vp-c-white); + --vp-button-brand-hover-bg: var(--vp-c-brand-2); + --vp-button-brand-active-border: transparent; + --vp-button-brand-active-text: var(--vp-c-white); + --vp-button-brand-active-bg: var(--vp-c-brand-1); +} + +/** + * Component: Home + * -------------------------------------------------------------------------- */ + +:root { + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: -webkit-linear-gradient( + 120deg, + #8b5cf6 30%, + #ec4899 + ); + + --vp-home-hero-image-background-image: linear-gradient( + -45deg, + #8b5cf6 50%, + #ec4899 50% + ); + --vp-home-hero-image-filter: blur(44px); +} diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts new file mode 100644 index 0000000..c2c257e --- /dev/null +++ b/docs/.vitepress/theme/index.ts @@ -0,0 +1,9 @@ +import DefaultTheme from 'vitepress/theme' +import './custom.css' + +export default { + extends: DefaultTheme, + enhanceApp({ app }) { + // Register global components if needed + } +} diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..664d572 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,104 @@ +# QuickForms Documentation + +This directory contains the VitePress documentation site for QuickForms. + +## Development + +```bash +# Start dev server +pnpm docs:dev + +# Build for production +pnpm docs:build + +# Preview production build +pnpm docs:preview +``` + +## Structure + +``` +docs/ +├── .vitepress/ +│ ├── config.ts # VitePress configuration +│ └── theme/ +│ ├── index.ts # Theme setup +│ └── custom.css # Custom styles +├── guide/ # User guides +│ ├── what-is-quickforms.md +│ ├── getting-started.md +│ ├── comparison.md +│ ├── schema-basics.md +│ ├── field-types.md +│ ├── validation.md +│ ├── complex-types.md +│ ├── custom-components.md +│ ├── theming.md +│ ├── custom-validators.md +│ ├── i18n.md +│ └── rbac.md +├── api/ # API reference +│ ├── form-options.md +│ ├── components.md +│ ├── composables.md +│ ├── schema-extensions.md +│ └── testers-registry.md +├── examples/ # Complete examples +│ ├── basic-form.md +│ ├── nested-objects.md +│ ├── arrays.md +│ ├── conditional-fields.md +│ ├── custom-validation.md +│ └── theming.md +├── packages/ # Package-specific docs +│ ├── core.md +│ ├── vue.md +│ └── quasar.md +├── public/ # Static assets +└── index.md # Homepage +``` + +## Adding New Pages + +1. Create a markdown file in the appropriate directory +2. Add the page to `.vitepress/config.ts` sidebar configuration +3. Use frontmatter for page metadata if needed + +## Deployment + +The docs can be deployed to GitHub Pages: + +```bash +# Build the docs +pnpm docs:build + +# The output will be in docs/.vitepress/dist +# Deploy this directory to your hosting provider +``` + +### GitHub Pages Setup + +1. Build the docs: `pnpm docs:build` +2. Deploy the `docs/.vitepress/dist` directory to the `gh-pages` branch +3. Enable GitHub Pages in repository settings + +Or use GitHub Actions for automatic deployment on push. + +## Features + +- ✅ Beautiful default theme +- ✅ Full-text search +- ✅ Mobile responsive +- ✅ Dark mode support +- ✅ Syntax highlighting +- ✅ Custom purple branding +- ✅ Automatic sidebar navigation +- ✅ Code group tabs + +## Contributing + +When adding new content: +- Keep examples concise and focused +- Include TypeScript types +- Add links to related pages +- Test code examples before committing diff --git a/docs/assets/quickforms-showcase-quasar.png b/docs/assets/quickforms-showcase-quasar.png new file mode 100644 index 0000000..1624aa0 Binary files /dev/null and b/docs/assets/quickforms-showcase-quasar.png differ diff --git a/docs/assets/quickforms-showcase-vue.png b/docs/assets/quickforms-showcase-vue.png new file mode 100644 index 0000000..f0db78d Binary files /dev/null and b/docs/assets/quickforms-showcase-vue.png differ diff --git a/docs/guide/comparison.md b/docs/guide/comparison.md new file mode 100644 index 0000000..d260112 --- /dev/null +++ b/docs/guide/comparison.md @@ -0,0 +1,140 @@ +# Comparison with JSONForms + +QuickForms takes inspiration from [JSONForms](https://jsonforms.io/)' excellent architecture but makes different trade-offs for modern Vue development. + +## Feature Comparison + +| Feature | QuickForms | JSONForms | +|---------|------------|-----------| +| **UI Schema** | Optional | Required for layouts | +| **Theming** | CSS variables (60+) | Framework-dependent | +| **Custom Validators** | Built-in (sync + async) | Not built-in | +| **Vue Version** | Vue 3 Composition API | Vue 2/3 Options API | +| **Bundle Size** | ~56KB gzipped | Varies by renderers | +| **TypeScript** | First-class support | Supported | +| **Role-Based Access** | Built-in | Custom implementation | +| **i18n Support** | Built-in label system | Via i18n library | +| **Validation Modes** | 3 modes (show/hide/none) | Single mode | +| **Status** | Early Stage | Mature & Battle-tested | + +## Philosophy Differences + +### JSONForms: Separation of Concerns +JSONForms strictly separates data schema (JSON Schema) from presentation (UI Schema). This is powerful for complex layouts but requires maintaining two schemas. + +```json +// JSONForms approach +{ + "schema": { /* data structure */ }, + "uischema": { // Required for layout + "type": "VerticalLayout", + "elements": [/* ... */] + } +} +``` + +### QuickForms: Sensible Defaults with Escape Hatches +QuickForms works without UI schemas, generating sensible layouts automatically. When you need customization, escape hatches are built into the data schema via `x-*` attributes. + +```typescript +// QuickForms approach +{ + "type": "object", + "properties": { + "status": { + "enum": ["draft", "active"], + "x-enum-labels": { // Escape hatch + "draft": "📝 Draft", + "active": "✅ Active" + } + } + } +} +``` + +## When to Choose JSONForms + +Choose JSONForms if you: +- Need battle-tested production reliability +- Require complex custom layouts (multi-column, tabs, etc.) +- Are working with React or Angular +- Need the UI Schema abstraction for design-dev separation +- Want a mature ecosystem with extensive documentation + +## When to Choose QuickForms + +Choose QuickForms if you: +- Use Vue 3 and want Composition API patterns +- Prefer rapid development with minimal configuration +- Need built-in RBAC and custom validation +- Want CSS variable theming without design system lock-in +- Prefer escape hatches over separate UI schemas +- Are building workflow engines or admin panels + +## Migration from JSONForms + +The core concepts translate well: + +### Component Registration +Both use a tester-based system for component selection. + +**JSONForms:** +```typescript +import { rankWith, isStringControl } from '@jsonforms/core' + +const tester = rankWith(10, isStringControl) +``` + +**QuickForms:** +```typescript +import { rankWith, isStringType } from '@quickflo/quickforms-vue' + +const tester = rankWith(10, isStringType) +``` + +### Custom Renderers +Both allow custom component registration. + +**JSONForms:** +```typescript +cells: [ + { tester: customTester, cell: CustomComponent } +] +``` + +**QuickForms:** +```typescript +registry.register('custom', CustomComponent, customTester) +``` + +### Validation +JSONForms relies purely on JSON Schema validation. QuickForms extends this with custom validators. + +**QuickForms:** +```vue + +``` + +## Can I Use Both? + +Yes! The core concepts are compatible: +- Both use JSON Schema for data structure +- Both use tester-based component selection +- Schemas are largely interchangeable + +You could even use the same JSON Schema with both libraries, adjusting only the UI Schema (JSONForms) or `x-*` attributes (QuickForms) as needed. + +## Acknowledgments + +QuickForms owes a great debt to JSONForms for pioneering the tester-based component registry system. We're standing on the shoulders of giants. diff --git a/docs/guide/complex-types.md b/docs/guide/complex-types.md new file mode 100644 index 0000000..c58acb0 --- /dev/null +++ b/docs/guide/complex-types.md @@ -0,0 +1,456 @@ +# Complex Types + +Learn how to work with nested objects, arrays, and conditional schemas in QuickForms. + +## Nested Objects + +Use `type: 'object'` with `properties` to create nested structures. + +### Basic Example + +```typescript +const schema = { + type: 'object', + properties: { + user: { + type: 'object', + title: 'User Information', + properties: { + firstName: { type: 'string', title: 'First Name' }, + lastName: { type: 'string', title: 'Last Name' }, + email: { type: 'string', format: 'email', title: 'Email' } + }, + required: ['firstName', 'lastName', 'email'] + }, + address: { + type: 'object', + title: 'Address', + properties: { + street: { type: 'string', title: 'Street' }, + city: { type: 'string', title: 'City' }, + state: { type: 'string', title: 'State' }, + zip: { type: 'string', pattern: '^\\d{5}$', title: 'ZIP Code' } + }, + required: ['street', 'city', 'state', 'zip'] + } + } +} +``` + +### Deep Nesting + +Objects can be nested to any depth: + +```typescript +{ + company: { + type: 'object', + properties: { + info: { + type: 'object', + properties: { + name: { type: 'string' }, + founded: { type: 'number' } + } + }, + address: { + type: 'object', + properties: { + headquarters: { + type: 'object', + properties: { + street: { type: 'string' }, + city: { type: 'string' } + } + } + } + } + } + } +} +``` + +## Arrays + +Use `type: 'array'` with `items` to define repeatable fields. + +### Array of Primitives + +```typescript +const schema = { + type: 'object', + properties: { + tags: { + type: 'array', + title: 'Tags', + items: { + type: 'string', + title: 'Tag' + }, + minItems: 1, + maxItems: 10 + }, + scores: { + type: 'array', + title: 'Scores', + items: { + type: 'number', + minimum: 0, + maximum: 100 + } + } + } +} +``` + +### Array of Objects + +```typescript +const schema = { + type: 'object', + properties: { + contacts: { + type: 'array', + title: 'Contacts', + items: { + type: 'object', + properties: { + name: { type: 'string', title: 'Name' }, + email: { type: 'string', format: 'email', title: 'Email' }, + phone: { type: 'string', title: 'Phone' } + }, + required: ['name', 'email'] + } + } + } +} +``` + +### Custom Array Item Labels + +Use `x-item-label` to customize how items are displayed: + +```typescript +{ + workHistory: { + type: 'array', + title: 'Work History', + 'x-item-label': '{{company}} - {{position}}', + items: { + type: 'object', + properties: { + company: { type: 'string', title: 'Company' }, + position: { type: 'string', title: 'Position' }, + startDate: { type: 'string', format: 'date', title: 'Start Date' }, + endDate: { type: 'string', format: 'date', title: 'End Date' } + } + } + } +} +``` + +### Array Validation + +```typescript +{ + team: { + type: 'array', + title: 'Team Members', + minItems: 2, // At least 2 members + maxItems: 10, // At most 10 members + uniqueItems: true, // No duplicates + items: { + type: 'string' + } + } +} +``` + +## Conditional Schemas (oneOf) + +Use `oneOf` when a field can match exactly one of several schemas. + +### Payment Method Example + +```typescript +const schema = { + type: 'object', + properties: { + paymentMethod: { + type: 'object', + title: 'Payment Method', + oneOf: [ + { + title: 'Credit Card', + properties: { + type: { const: 'credit_card' }, + cardNumber: { + type: 'string', + pattern: '^\\d{16}$', + title: 'Card Number' + }, + cvv: { + type: 'string', + pattern: '^\\d{3}$', + title: 'CVV' + }, + expiryDate: { + type: 'string', + pattern: '^(0[1-9]|1[0-2])\\/\\d{2}$', + title: 'Expiry (MM/YY)' + } + }, + required: ['cardNumber', 'cvv', 'expiryDate'] + }, + { + title: 'PayPal', + properties: { + type: { const: 'paypal' }, + email: { + type: 'string', + format: 'email', + title: 'PayPal Email' + } + }, + required: ['email'] + }, + { + title: 'Bank Transfer', + properties: { + type: { const: 'bank_transfer' }, + accountNumber: { type: 'string', title: 'Account Number' }, + routingNumber: { type: 'string', title: 'Routing Number' } + }, + required: ['accountNumber', 'routingNumber'] + } + ] + } + } +} +``` + +### How oneOf Works + +1. User selects which schema to use (dropdown or tabs) +2. Form displays only the fields for the selected schema +3. Validation ensures data matches exactly one schema + +### Discriminator with `const` + +Use `const` to create a type discriminator: + +```typescript +{ + contact: { + oneOf: [ + { + title: 'Email Contact', + properties: { + method: { const: 'email' }, // Discriminator + email: { type: 'string', format: 'email' } + } + }, + { + title: 'Phone Contact', + properties: { + method: { const: 'phone' }, // Discriminator + phone: { type: 'string' } + } + } + ] + } +} +``` + +## anyOf + +Use `anyOf` when data can match one or more schemas. + +```typescript +{ + notifications: { + type: 'object', + title: 'Notification Settings', + anyOf: [ + { + properties: { + email: { type: 'boolean', title: 'Email Notifications' }, + emailAddress: { type: 'string', format: 'email' } + } + }, + { + properties: { + sms: { type: 'boolean', title: 'SMS Notifications' }, + phoneNumber: { type: 'string' } + } + } + ] + } +} +``` + +**Difference from oneOf:** With `anyOf`, multiple schemas can match simultaneously. + +## allOf + +Use `allOf` to merge multiple schemas into one. + +```typescript +{ + user: { + allOf: [ + { + // Base user info + type: 'object', + properties: { + name: { type: 'string' }, + email: { type: 'string', format: 'email' } + }, + required: ['name', 'email'] + }, + { + // Additional fields + type: 'object', + properties: { + age: { type: 'number', minimum: 18 }, + country: { type: 'string' } + } + } + ] + } +} +``` + +**Use Cases:** +- Composition: Combine base schemas with extensions +- Inheritance: Add fields to a base type +- Mixins: Merge common field sets + +## Combining Complex Types + +You can combine nested objects, arrays, and conditional schemas: + +```typescript +const schema = { + type: 'object', + properties: { + projects: { + type: 'array', + title: 'Projects', + items: { + type: 'object', + properties: { + name: { type: 'string', title: 'Project Name' }, + members: { + type: 'array', + title: 'Team Members', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + role: { + type: 'string', + enum: ['developer', 'designer', 'manager'] + }, + contact: { + oneOf: [ + { + title: 'Email', + properties: { + type: { const: 'email' }, + email: { type: 'string', format: 'email' } + } + }, + { + title: 'Phone', + properties: { + type: { const: 'phone' }, + phone: { type: 'string' } + } + } + ] + } + } + } + } + } + } + } + } +} +``` + +This creates: **Array of objects** → each with **array of objects** → each with **conditional field**. + +## Default Values + +Set default values for complex types: + +```typescript +{ + preferences: { + type: 'object', + default: { + theme: 'light', + notifications: true + }, + properties: { + theme: { type: 'string', enum: ['light', 'dark'] }, + notifications: { type: 'boolean' } + } + }, + tags: { + type: 'array', + default: ['javascript', 'vue'], + items: { type: 'string' } + } +} +``` + +Enable defaults in form options: + +```vue + +``` + +## Required Fields in Nested Objects + +Each object level has its own `required` array: + +```typescript +{ + user: { + type: 'object', + properties: { + name: { type: 'string' }, + email: { type: 'string' } + }, + required: ['name', 'email'] // Required at this level + }, + address: { + type: 'object', + properties: { + street: { type: 'string' }, + city: { type: 'string' } + }, + required: ['city'] // Only city is required + } +} +``` + +## Best Practices + +1. **Keep nesting shallow** - Deeply nested forms are hard to use +2. **Use meaningful titles** - Help users understand structure +3. **Provide defaults** - Especially for nested objects +4. **Validate arrays** - Use minItems/maxItems appropriately +5. **Use oneOf sparingly** - Only when truly mutually exclusive +6. **Label array items** - Use `x-item-label` for better UX +7. **Test deeply nested forms** - Ensure validation works at all levels + +## Next Steps + +- [Schema Basics](/guide/schema-basics) - Fundamental schema concepts +- [Validation](/guide/validation) - Validation for complex types +- [Schema Extensions](/guide/schema-extensions) - `x-item-label` and more +- [Examples](/guide/examples/nested-objects) - Working examples diff --git a/docs/guide/components.md b/docs/guide/components.md new file mode 100644 index 0000000..542b862 --- /dev/null +++ b/docs/guide/components.md @@ -0,0 +1,530 @@ +# Components API + +QuickForms provides pre-built components for all standard JSON Schema types. + +## DynamicForm + +Main form component that generates fields from JSON Schema. + +### Props + +```typescript +interface DynamicFormProps { + schema: JSONSchema // JSON Schema definition + modelValue: any // Form data (v-model) + options?: FormOptions // Form configuration +} +``` + +### Events + +```typescript +interface DynamicFormEmits { + 'update:modelValue': (value: any) => void // Form data changed + 'validation': (result: ValidationResult) => void // Validation state changed +} + +interface ValidationResult { + valid: boolean + errors: Record +} +``` + +### Example + +```vue + + + +``` + +### Slots + +None. The `DynamicForm` component automatically renders fields based on the schema. For customization, use the [component registry](/guide/custom-components). + +--- + +## FieldRenderer + +Internal component that selects and renders the appropriate field component based on schema. + +### Props + +```typescript +interface FieldRendererProps { + schema: JSONSchema + path: string +} +``` + +### Usage + +Used internally by `DynamicForm` and container fields (`ObjectField`, `ArrayField`, `OneOfField`). You typically won't use this directly unless building a custom layout component. + +```vue + +``` + +--- + +## Field Components + +Pre-built components for each field type. All field components share the same props interface: + +### Common Props + +```typescript +interface FieldProps { + schema: JSONSchema // Field's JSON Schema + path: string // Field path in form data + disabled?: boolean // Disable field input + readonly?: boolean // Make field read-only +} +``` + +--- + +## StringField + +Renders text inputs for string fields. + +### Handles + +- `type: 'string'` +- Various formats: `email`, `url`, `password`, `textarea` + +### Example Schema + +```typescript +{ + type: 'string', + title: 'Full Name', + minLength: 2, + maxLength: 100 +} +``` + +**Format variants:** +```typescript +// Email +{ type: 'string', format: 'email' } + +// URL +{ type: 'string', format: 'url' } + +// Password +{ type: 'string', format: 'password' } + +// Textarea +{ type: 'string', format: 'textarea' } +``` + +--- + +## NumberField + +Renders number inputs. + +### Handles + +- `type: 'number'` +- `type: 'integer'` + +### Example Schema + +```typescript +{ + type: 'number', + title: 'Age', + minimum: 0, + maximum: 120 +} +``` + +```typescript +{ + type: 'integer', + title: 'Quantity', + multipleOf: 5 +} +``` + +--- + +## BooleanField + +Renders checkboxes for boolean values. + +### Handles + +- `type: 'boolean'` + +### Example Schema + +```typescript +{ + type: 'boolean', + title: 'Accept Terms', + default: false +} +``` + +--- + +## EnumField + +Renders select dropdowns for enum values. + +### Handles + +- Fields with `enum` property + +### Example Schema + +```typescript +{ + type: 'string', + title: 'Country', + enum: ['US', 'CA', 'UK', 'DE'], + 'x-enum-labels': { + US: 'United States', + CA: 'Canada', + UK: 'United Kingdom', + DE: 'Germany' + } +} +``` + +**Multiple selection:** +```typescript +{ + type: 'array', + title: 'Tags', + items: { + type: 'string', + enum: ['javascript', 'typescript', 'vue', 'react'] + }, + uniqueItems: true +} +``` + +--- + +## DateField + +Renders date/time pickers. + +### Handles + +- `format: 'date'` +- `format: 'time'` +- `format: 'date-time'` + +### Example Schema + +```typescript +// Date +{ + type: 'string', + format: 'date', + title: 'Birth Date' +} + +// Time +{ + type: 'string', + format: 'time', + title: 'Appointment Time' +} + +// DateTime +{ + type: 'string', + format: 'date-time', + title: 'Event Start' +} +``` + +--- + +## ObjectField + +Renders nested object fields. + +### Handles + +- `type: 'object'` + +### Example Schema + +```typescript +{ + type: 'object', + title: 'Address', + properties: { + street: { type: 'string', title: 'Street' }, + city: { type: 'string', title: 'City' }, + zip: { type: 'string', title: 'ZIP Code' } + }, + required: ['street', 'city'] +} +``` + +**Rendering:** +- Plain Vue: Simple wrapper with nested fields +- Quasar: Expandable card with `QExpansionItem` + +--- + +## ArrayField + +Renders dynamic array fields with add/remove buttons. + +### Handles + +- `type: 'array'` + +### Example Schema + +```typescript +// Simple array +{ + type: 'array', + title: 'Tags', + items: { type: 'string' } +} + +// Array of objects +{ + type: 'array', + title: 'Contacts', + items: { + type: 'object', + properties: { + name: { type: 'string', title: 'Name' }, + phone: { type: 'string', title: 'Phone' } + } + }, + minItems: 1, + maxItems: 5 +} +``` + +**Features:** +- Add/remove buttons +- Respects `minItems` and `maxItems` +- Drag-to-reorder (if enabled) + +--- + +## OneOfField + +Renders conditional schemas with a selector. + +### Handles + +- Schemas with `oneOf` + +### Example Schema + +```typescript +{ + oneOf: [ + { + type: 'object', + title: 'Individual', + properties: { + name: { type: 'string', title: 'Full Name' }, + ssn: { type: 'string', title: 'SSN' } + } + }, + { + type: 'object', + title: 'Business', + properties: { + company: { type: 'string', title: 'Company Name' }, + ein: { type: 'string', title: 'EIN' } + } + } + ] +} +``` + +**Rendering:** +- Dropdown to select which schema to use +- Dynamically renders fields based on selection + +--- + +## AllOfField + +Renders merged schemas (intersection). + +### Handles + +- Schemas with `allOf` + +### Example Schema + +```typescript +{ + allOf: [ + { + type: 'object', + properties: { + name: { type: 'string', title: 'Name' } + } + }, + { + type: 'object', + properties: { + email: { type: 'string', format: 'email', title: 'Email' } + } + } + ] +} +``` + +**Behavior:** +- Merges all schemas in `allOf` array +- Renders as a single flat object + +--- + +## HiddenField + +Renders a hidden input (no visual output). + +### Handles + +- Fields with `'x-hidden': true` + +### Example Schema + +```typescript +{ + type: 'string', + 'x-hidden': true, + default: 'hidden-value' +} +``` + +**Use Cases:** +- Hidden form fields +- Tracking fields +- Server-side data + +--- + +## Customizing Built-in Components + +You cannot modify built-in components directly, but you can: + +### 1. Override with Custom Component + +Register a custom component with higher priority: + +```typescript +import { createDefaultRegistry, rankWith, isStringType } from '@quickflo/quickforms-vue' +import CustomStringField from './CustomStringField.vue' + +const registry = createDefaultRegistry() + +// Override default StringField (priority 2) with priority 20 +registry.register('custom-string', CustomStringField, rankWith(20, isStringType)) +``` + +### 2. Use Component Defaults (Quasar) + +Configure global defaults for all components: + +```typescript +import { createQuasarRegistry } from '@quickflo/quickforms-quasar' + +const options = { + registry: createQuasarRegistry(), + componentDefaults: { + global: { + outlined: true, + dense: true + }, + input: { + clearable: true + } + } +} +``` + +### 3. Per-Field Overrides + +Use schema extensions: + +```typescript +{ + type: 'string', + title: 'Email', + 'x-component-props': { + placeholder: 'you@example.com', + autocomplete: 'email' + } +} +``` + +--- + +## Component Props Reference + +All field components receive these props automatically: + +| Prop | Type | Description | +|------|------|-------------| +| `schema` | `JSONSchema` | Field's JSON Schema definition | +| `path` | `string` | Field path (e.g., `"email"`, `"user.name"`) | +| `disabled` | `boolean` | Form-level disabled state | +| `readonly` | `boolean` | Form-level readonly state | + +Field components use `useFormField(path, schema)` to access: +- `value` - Reactive field value +- `errorMessage` - Validation error +- `label` - Display label +- `hint` - Hint text +- `required` - Required status + +See [Composables API](/guide/composables) for details. + +--- + +## Next Steps + +- [Custom Components](/guide/custom-components) - Register your own components +- [Composables API](/guide/composables) - Build custom field components +- [Testers & Registry](/guide/testers-registry) - Component selection system diff --git a/docs/guide/composables.md b/docs/guide/composables.md new file mode 100644 index 0000000..ecdb0c1 --- /dev/null +++ b/docs/guide/composables.md @@ -0,0 +1,361 @@ +# Composables API + +QuickForms provides Vue 3 composables for building custom components and accessing form state. + +## useFormField + +Hook for managing individual field state with automatic validation and label handling. + +### Signature + +```typescript +function useFormField( + path: string, + schema: JSONSchema, + options?: { label?: string } +): UseFormFieldReturn +``` + +### Parameters + +- **`path`** - Field path in form data (e.g., `"email"`, `"user.address.city"`) +- **`schema`** - JSON Schema definition for this field +- **`options`** - Optional configuration + - `label` - Override the field label (defaults to `schema.title` or `path`) + +### Return Value + +```typescript +interface UseFormFieldReturn { + value: Ref // Reactive field value (v-model compatible) + errorMessage: ComputedRef // Current validation error + errors: Ref // All validation errors + setValue: (val: any) => void // Programmatically set value + setTouched: (touched: boolean) => void // Mark field as touched + meta: FieldMeta // VeeValidate field metadata + label: ComputedRef // Display label + hint: ComputedRef // Hint text + hintMode: ComputedRef<'always' | 'focus' | 'hover'> // When to show hint + required: ComputedRef // Whether field is required +} +``` + +### Example Usage + +```vue + + + +``` + +### Features + +**Automatic Validation:** +- JSON Schema validation (type, format, min/max, pattern, etc.) +- Custom validators from form options +- Async validator support with debouncing +- Custom error messages from `x-error-messages` or form options + +**Smart Error Display:** +- Respects `validationMode` from form options +- `ValidateAndShow` - Shows errors as you type +- `ValidateAndHide` - Validates silently (returns `null` for errorMessage) +- `NoValidation` - Skips all validation + +**Hint Management:** +- Reads from `x-hint` or `description` +- Supports HTML in `x-hint` +- Applies `hintRenderer` if provided in form options +- Respects `x-hint-mode` or global `componentDefaults.hints.showMode` + +**Label Resolution:** +- Uses override from `options.label` if provided +- Falls back to `schema.title` +- Falls back to `path` + +### Advanced: Custom Validators + +The composable automatically runs custom validators from form options: + +```typescript +const options = { + validators: { + email: async (value, allValues, context) => { + const response = await fetch(`/guide/check-email?email=${value}`) + const { available } = await response.json() + return available || 'Email already taken' + } + }, + validatorDebounce: { + email: 500 // Debounce async validation by 500ms + } +} +``` + +### Advanced: Programmatic Control + +```vue + +``` + +--- + +## useFormContext + +Access form-level context and configuration. + +### Signature + +```typescript +function useFormContext(): FormContext | undefined +``` + +### Return Value + +```typescript +interface FormContext { + schema: JSONSchema // Root schema + validationMode: ValidationMode // Current validation mode + readonly?: boolean // Form-level readonly + disabled?: boolean // Form-level disabled + errorMessages?: ErrorMessages // Custom error messages + validators?: ValidatorMap // Custom validators + validatorDebounce?: number | Record + hintRenderer?: HintRenderer // Custom hint renderer + componentDefaults?: ComponentDefaults + context?: Record // User-provided context + formValues?: () => any // Get all form values +} +``` + +### Example Usage + +```vue + +``` + +### Use Cases + +**1. Custom Components:** + +Access form configuration in custom field components: + +```vue + +``` + +**2. Cross-Field Logic:** + +Access other field values for dependent validation: + +```vue + +``` + +**3. Role-Based UI:** + +Show/hide elements based on user context: + +```vue + + + +``` + +--- + +## provideFormContext + +Provide form context to child components (used internally by `DynamicForm`). + +### Signature + +```typescript +function provideFormContext(context: FormContext): void +``` + +### Usage + +This is primarily used internally by the `DynamicForm` component. You typically won't need to call this directly unless building a custom form wrapper. + +```typescript +import { provideFormContext } from '@quickflo/quickforms-vue' + +// In a custom form wrapper component +provideFormContext({ + schema: mySchema, + validationMode: 'ValidateAndShow', + readonly: false, + // ... other context +}) +``` + +--- + +## Utility Functions + +### getHint + +Extract hint text from schema (used internally by `useFormField`). + +```typescript +function getHint(schema: JSONSchema): string | undefined +``` + +Returns `x-hint` if present, otherwise `description`. + +### Example + +```typescript +import { getHint } from '@quickflo/quickforms-vue' + +const hint = getHint({ + type: 'string', + description: 'Plain text hint', + 'x-hint': 'HTML hint with link' +}) + +console.log(hint) // "HTML hint with link" +``` + +--- + +## Type Definitions + +### ValidationMode + +```typescript +type ValidationMode = 'ValidateAndShow' | 'ValidateAndHide' | 'NoValidation' +``` + +### FieldMeta + +From VeeValidate: + +```typescript +interface FieldMeta { + touched: boolean // Has field been focused? + dirty: boolean // Has value changed from initial? + valid: boolean // Is field valid? + pending: boolean // Is async validation running? + initialValue: any // Original value +} +``` + +### HintRenderer + +Custom function to transform hint text: + +```typescript +type HintRenderer = ( + hint: string, + context: { + schema: JSONSchema + path: string + value: any + } +) => string +``` + +### ValidatorMap + +Custom validators keyed by field path: + +```typescript +type ValidatorMap = Record + +type ValidatorFunction = ( + value: any, + allValues: Record, + context: Record +) => boolean | string | Promise | ValidationResult + +interface ValidationResult { + valid: boolean + message?: string +} +``` + +--- + +## Next Steps + +- [Custom Components](/guide/custom-components) - Build custom fields using these composables +- [Custom Validators](/guide/custom-validators) - Add custom validation logic +- [Form Options](/guide/form-options) - Configure form-level behavior diff --git a/docs/guide/custom-components.md b/docs/guide/custom-components.md new file mode 100644 index 0000000..5f8350f --- /dev/null +++ b/docs/guide/custom-components.md @@ -0,0 +1,464 @@ +# Custom Components + +Register your own components for specific fields using QuickForms' tester system. + +## Why Custom Components? + +QuickForms provides built-in components for all standard field types. Use custom components when you need: + +- **Custom UI/UX** - Special input widgets (color picker, rich text editor, etc.) +- **Integration** - Third-party libraries (date pickers, map selectors, etc.) +- **Branding** - Company-specific styled components +- **Business logic** - Domain-specific input patterns + +## Component Registry + +QuickForms uses a **component registry** with a **tester priority system** to select which component renders each field. + +### How It Works + +1. For each field, QuickForms runs all registered testers +2. Testers return a priority number (higher = better match) +3. The component with the highest priority renders the field + +This is the same pattern used by JSONForms. + +## Basic Example + +### 1. Create Your Component + +```vue + + + + + + +``` + +### 2. Create a Tester + +```typescript +import { rankWith, isStringType, hasFormat } from '@quickflo/quickforms-vue' +import type { JSONSchema } from '@quickflo/quickforms' + +// Match string fields with format: "phone" +export const isPhoneField = (schema: JSONSchema): number => { + return rankWith(10, (s) => isStringType(s) && hasFormat('phone')(s))(schema) +} +``` + +### 3. Register the Component + +```typescript +import { createDefaultRegistry } from '@quickflo/quickforms-vue' +import PhoneInput from './PhoneInput.vue' +import { isPhoneField } from './testers' + +const registry = createDefaultRegistry() + +// Register with tester +registry.register('phone', PhoneInput, isPhoneField) + +// Use in form +const options = { + registry +} +``` + +### 4. Use in Schema + +```typescript +const schema = { + type: 'object', + properties: { + phone: { + type: 'string', + format: 'phone', // This triggers your custom component! + title: 'Phone Number' + } + } +} +``` + +## Tester Functions + +Testers determine when a component should be used. + +### Built-in Testers + +```typescript +import { + isStringType, + isNumberType, + isBooleanType, + isObjectType, + isArrayType, + hasFormat, + hasOneOf, + hasAnyOf, + hasAllOf, + isEnum, + isRequired, + rankWith +} from '@quickflo/quickforms-vue' + +// Examples +isStringType(schema) // true if type: 'string' +isNumberType(schema) // true if type: 'number' +hasFormat('email')(schema) // true if format: 'email' +isEnum(schema) // true if has enum property +hasOneOf(schema) // true if has oneOf property +``` + +### Combining Testers + +Use `and`, `or`, `not` to combine testers: + +```typescript +import { and, or, not, rankWith, isStringType, hasFormat } from '@quickflo/quickforms-vue' + +// Match string with email OR url format +const isEmailOrUrl = rankWith(5, or( + hasFormat('email'), + hasFormat('url') +)) + +// Match string without a format +const isPlainString = rankWith(5, and( + isStringType, + not(hasFormat()) +)) +``` + +### Priority Ranking + +Use `rankWith()` to set priority: + +```typescript +// Higher number = higher priority + +const lowPriority = rankWith(1, isStringType) // Fallback +const mediumPriority = rankWith(5, hasFormat('email')) // Specific format +const highPriority = rankWith(10, customLogic) // Very specific + +// If multiple testers match, highest priority wins +``` + +**Built-in component priorities:** +- Generic types (string, number): Priority 1-2 +- Specific formats (email, date): Priority 3-5 +- Complex types (object, array): Priority 5-10 +- Your custom components: Usually 10-15 + +## Advanced Examples + +### Custom Date Picker + +```vue + + + + +``` + +```typescript +// Register for date format +import { rankWith, isStringType, hasFormat, and } from '@quickflo/quickforms-vue' + +const isDateField = rankWith(15, (schema) => + isStringType(schema) && hasFormat('date')(schema) +) + +registry.register('custom-date', DatePicker, isDateField) +``` + +### Custom Enum with Icons + +```vue + + + + +``` + +### Conditional Registration + +Only register component if a library is available: + +```typescript +import { createDefaultRegistry } from '@quickflo/quickforms-vue' + +const registry = createDefaultRegistry() + +// Only register if library is installed +if (typeof window !== 'undefined' && window.MyCustomLibrary) { + import('./CustomComponent.vue').then((mod) => { + registry.register('custom', mod.default, tester) + }) +} +``` + +## Quasar Custom Components + +For Quasar projects, start with the Quasar registry: + +```typescript +import { createQuasarRegistry } from '@quickflo/quickforms-quasar' +import CustomPhoneInput from './CustomPhoneInput.vue' + +const registry = createQuasarRegistry() + +// Add your custom component +registry.register('phone', CustomPhoneInput, isPhoneField) + +// Use in boot file +export const options = { + registry, + // ... other options +} +``` + +## Component Props + +Your custom component receives these props: + +```typescript +interface FieldComponentProps { + schema: JSONSchema // Field's JSON Schema + path: string // Field path (e.g., "user.email") + disabled?: boolean // Form-level disabled state + readonly?: boolean // Form-level readonly state +} +``` + +## useFormField Composable + +The `useFormField` composable handles all the boilerplate: + +```typescript +const { + value, // Reactive field value (v-model compatible) + errorMessage, // Current validation error + label, // Field label from schema.title + hint, // Hint text from schema.description or x-hint + required, // Whether field is required + disabled, // Computed disabled state + readonly // Computed readonly state +} = useFormField(path, schema) +``` + +## Overriding Built-in Components + +Register with higher priority to override: + +```typescript +import { createDefaultRegistry } from '@quickflo/quickforms-vue' +import CustomStringField from './CustomStringField.vue' + +const registry = createDefaultRegistry() + +// Override default string component (priority 2) with higher priority +const isMyString = rankWith(20, isStringType) + +registry.register('custom-string', CustomStringField, isMyString) +``` + +## Schema-Based Selection + +Use custom schema properties to trigger components: + +```typescript +// Schema with custom property +{ + type: 'string', + 'x-widget': 'color-picker' +} + +// Tester that checks custom property +const isColorPicker = rankWith(15, (schema) => + schema['x-widget'] === 'color-picker' +) +``` + +## Best Practices + +1. **Use high priority** - Set 10+ to override built-ins +2. **Keep testers simple** - Fast checks only +3. **Use `useFormField`** - Handles validation and labels +4. **Test thoroughly** - Especially validation behavior +5. **Document extensions** - If using custom `x-*` properties +6. **Consider accessibility** - ARIA labels, keyboard navigation +7. **Handle edge cases** - null, undefined, disabled states + +## Complete Example + +See this complete workflow: + +```typescript +// 1. Component +// MySlider.vue - Custom range slider + + + + +// 2. Tester +import { rankWith, isNumberType } from '@quickflo/quickforms-vue' + +const isSlider = rankWith(10, (schema) => + isNumberType(schema) && schema['x-widget'] === 'slider' +) + +// 3. Registration +import { createDefaultRegistry } from '@quickflo/quickforms-vue' +import MySlider from './MySlider.vue' + +const registry = createDefaultRegistry() +registry.register('slider', MySlider, isSlider) + +// 4. Usage +const schema = { + type: 'object', + properties: { + volume: { + type: 'number', + title: 'Volume', + minimum: 0, + maximum: 100, + 'x-widget': 'slider' + } + } +} + +const options = { registry } +``` + +## Next Steps + +- [Testers & Registry API](/guide/testers-registry) - Complete API reference +- [Schema Extensions](/guide/schema-extensions) - Custom `x-*` properties +- [Composables API](/guide/composables) - `useFormField` details diff --git a/docs/guide/custom-validators.md b/docs/guide/custom-validators.md new file mode 100644 index 0000000..c859a77 --- /dev/null +++ b/docs/guide/custom-validators.md @@ -0,0 +1,231 @@ +# Custom Validators + +Add custom validation logic beyond JSON Schema capabilities. Supports both sync and async validators. + +## Sync Validators + +Perfect for cross-field validation or business logic that doesn't require API calls. + +### Password Confirmation Example + +```vue + + + +``` + +### Business Logic Example + +```typescript +validators: { + // Age must match birthdate + age: (value, allValues) => { + if (!allValues.birthdate) return true + + const birthYear = new Date(allValues.birthdate).getFullYear() + const calculatedAge = new Date().getFullYear() - birthYear + + if (Math.abs(value - calculatedAge) > 1) { + return 'Age doesn\'t match birth date' + } + return true + }, + + // Conditional required + otherSpecify: (value, allValues) => { + if (allValues.category === 'other' && !value) { + return 'Please specify when selecting "Other"' + } + return true + } +} +``` + +## Async Validators + +Perfect for API validation like username availability or email verification. + +### Username Availability Example + +```vue + + + +``` + +### Email Verification Example + +```typescript +validators: { + email: async (value) => { + if (!value) return true + + // Check email format first (JSON Schema handles this) + // Then verify domain + const response = await fetch('/guide/verify-email', { + method: 'POST', + body: JSON.stringify({ email: value }) + }) + + const result = await response.json() + return result.valid || 'Email domain not allowed' + } +} +``` + +## Debouncing + +Control how long to wait after user stops typing before running async validators. + +### Global Debounce + +```typescript +{ + validatorDebounce: 300 // Apply to all async validators +} +``` + +### Per-Field Debounce + +```typescript +{ + validatorDebounce: { + username: 500, // Slower API + email: 300, // Faster API + zipCode: 1000 // Very slow API + } +} +``` + +## Return Values + +Validators can return multiple formats: + +```typescript +// Valid +return true + +// Invalid with generic error +return false + +// Invalid with custom message +return 'Username already taken' + +// Object format +return { valid: false, message: 'Custom error' } + +// Async (return Promise of any above) +return Promise.resolve(true) +``` + +## Validator Function Signature + +```typescript +type ValidatorFunction = ( + value: any, // Current field value + allValues: Record, // All form values + context?: Record // Form context (e.g., user roles) +) => boolean | string | Promise +``` + +## Using Context + +Access application context in validators: + +```vue + +``` + +## Best Practices + +1. **Return early** for empty values if field is optional +2. **Use debouncing** for async validators to avoid excessive API calls +3. **Provide clear error messages** - return strings instead of false +4. **Keep validators pure** - no side effects +5. **Test edge cases** - null, undefined, empty strings + +## Next Steps + +- [Validation Guide](/guide/validation) - Validation modes and error messages +- [Form Options API](/guide/form-options) - Complete validator options diff --git a/docs/guide/examples/arrays.md b/docs/guide/examples/arrays.md new file mode 100644 index 0000000..774cfbc --- /dev/null +++ b/docs/guide/examples/arrays.md @@ -0,0 +1,462 @@ +# Arrays Example + +Working with dynamic arrays in QuickForms - add, remove, and reorder items. + +## Simple Array Example + +```vue + + + +``` + +## Array of Objects + +```vue + + + +``` + +## Quasar Array with Custom Buttons + +```vue + + + +``` + +## Nested Arrays + +Arrays can contain other arrays: + +```typescript +const schema: JSONSchema = { + type: 'object', + properties: { + departments: { + type: 'array', + title: 'Departments', + items: { + type: 'object', + properties: { + name: { + type: 'string', + title: 'Department Name' + }, + projects: { + type: 'array', + title: 'Projects', + items: { + type: 'object', + properties: { + projectName: { + type: 'string', + title: 'Project Name' + }, + tasks: { + type: 'array', + title: 'Tasks', + items: { + type: 'string' + } + } + } + } + } + } + } + } + } +} + +// Resulting structure: +// { +// departments: [ +// { +// name: 'Engineering', +// projects: [ +// { +// projectName: 'Website Redesign', +// tasks: ['Design mockups', 'Build components', 'Testing'] +// } +// ] +// } +// ] +// } +``` + +## Array Validation + +### minItems / maxItems + +```typescript +{ + tags: { + type: 'array', + items: { type: 'string' }, + minItems: 2, // Must have at least 2 items + maxItems: 10 // Cannot have more than 10 items + } +} +``` + +### uniqueItems + +Ensure all items are unique: + +```typescript +{ + selectedOptions: { + type: 'array', + items: { + type: 'string', + enum: ['option1', 'option2', 'option3'] + }, + uniqueItems: true // No duplicates allowed + } +} +``` + +### Item Validation + +Each item is validated according to its schema: + +```typescript +{ + ages: { + type: 'array', + items: { + type: 'number', + minimum: 0, + maximum: 120 + } + } +} +``` + +## Working with Array Data + +### Programmatic Access + +```vue + +``` + +### Prefill Array Data + +```typescript +const formData = ref({ + contacts: [ + { name: 'Alice', email: 'alice@example.com' }, + { name: 'Bob', email: 'bob@example.com' }, + { name: 'Charlie', email: 'charlie@example.com' } + ] +}) +``` + +## Multi-Select Enum Arrays + +For multiple selection from a list: + +```typescript +{ + interests: { + type: 'array', + title: 'Interests', + items: { + type: 'string', + enum: ['sports', 'music', 'reading', 'gaming', 'cooking', 'travel'] + }, + uniqueItems: true, + 'x-enum-labels': { + sports: 'Sports & Fitness', + music: 'Music & Concerts', + reading: 'Reading & Books', + gaming: 'Video Games', + cooking: 'Cooking & Food', + travel: 'Travel & Adventure' + } + } +} +``` + +With Quasar, this renders as a multi-select dropdown with chips. + +## Tips + +1. **Default Values**: Always provide initial array in `formData` (empty or with defaults) +2. **Min/Max Items**: Use `minItems`/`maxItems` to enforce array size constraints +3. **Unique Items**: Use `uniqueItems: true` for multi-select scenarios +4. **Custom Buttons**: In Quasar, use `x-quickforms-quasar` to customize add/remove buttons +5. **Deep Watch**: Use `{ deep: true }` when watching arrays for changes +6. **Reordering**: Array items can be reordered (drag-and-drop support varies by package) + +## Common Patterns + +### Todo List + +```typescript +{ + todos: { + type: 'array', + title: 'To-Do Items', + items: { + type: 'object', + properties: { + task: { type: 'string', title: 'Task' }, + completed: { type: 'boolean', title: 'Completed', default: false }, + priority: { + type: 'string', + enum: ['low', 'medium', 'high'], + default: 'medium' + } + } + } + } +} +``` + +### Education History + +```typescript +{ + education: { + type: 'array', + title: 'Education', + items: { + type: 'object', + properties: { + institution: { type: 'string', title: 'School/University' }, + degree: { type: 'string', title: 'Degree' }, + major: { type: 'string', title: 'Major' }, + startDate: { type: 'string', format: 'date', title: 'Start Date' }, + endDate: { type: 'string', format: 'date', title: 'End Date' }, + gpa: { type: 'number', minimum: 0, maximum: 4, title: 'GPA' } + } + } + } +} +``` + +## Next Steps + +- [Conditional Fields](/guide/guide/examples/conditional-fields) - Dynamic forms based on user input +- [Custom Validation](/guide/guide/examples/custom-validation) - Add custom validation logic +- [Complex Types](/guide/complex-types) - Advanced schema features diff --git a/docs/guide/examples/basic-form.md b/docs/guide/examples/basic-form.md new file mode 100644 index 0000000..b4a1004 --- /dev/null +++ b/docs/guide/examples/basic-form.md @@ -0,0 +1,252 @@ +# Basic Form Example + +A simple contact form demonstrating the fundamentals of QuickForms. + +## Quasar Example + +```vue + + + +``` + +## Plain Vue Example + +If you're not using Quasar, use the plain Vue package: + +```vue + + + + + +``` + +## Key Differences + +**Quasar version:** +- Uses `createQuasarRegistry()` to get pre-styled Quasar components +- Fields render as `QInput`, `QSelect`, `QCheckbox` automatically +- Supports `x-hint` for hints (instead of `description`) +- Uses `componentDefaults` for consistent styling (outlined, dense) +- Wraps in `q-page` with Quasar spacing utilities + +**Plain Vue version:** +- No registry needed - uses default components +- Fields render as plain HTML inputs with custom styling +- Uses `description` for hints +- Custom CSS for styling + +## What's Happening? + +### Schema Definition +```typescript +const schema: JSONSchema = { + type: 'object', // Root type + properties: { /* ... */ }, // Field definitions + required: ['name', 'email', 'age'] // Required fields +} +``` + +### Field Types +- **`name`** - String with minimum length validation +- **`email`** - String with email format validation +- **`age`** - Number with min/max constraints +- **`newsletter`** - Boolean rendered as checkbox +- **`role`** - Enum rendered as select dropdown + +### Validation +QuickForms automatically: +- Validates email format +- Checks minimum string length +- Validates number ranges +- Shows required field indicators (*) +- Displays error messages as you type + +### Two-Way Binding +The `v-model` directive creates reactive two-way data binding: + +```vue + +``` + +Changes to the form update `formData`, and programmatic updates to `formData` update the form. + +### Using Form Data +Since data is available via `v-model`, you can use it however you want: + +```typescript +const saveForm = () => { + // formData.value contains all form data + await api.post('/contacts', formData.value) +} + +const resetForm = () => { + formData.value = {} +} +``` + +## Try It Yourself + +1. Leave required fields empty and try to submit +2. Enter an invalid email address +3. Set age below 18 or above 120 +4. Watch the form data update in real-time + +## Next Steps + +- [Nested Objects](/guide/examples/nested-objects) - Handle complex object structures +- [Arrays](/guide/examples/arrays) - Work with dynamic lists +- [Custom Validation](/guide/examples/custom-validation) - Add your own validation logic diff --git a/docs/guide/examples/conditional-fields.md b/docs/guide/examples/conditional-fields.md new file mode 100644 index 0000000..b4bb425 --- /dev/null +++ b/docs/guide/examples/conditional-fields.md @@ -0,0 +1,377 @@ +# Conditional Fields Example + +Create dynamic forms where fields appear based on other field values using `oneOf`. + +## Simple Conditional Example + +```vue + + + +``` + +## Payment Method Selector + +```typescript +const schema: JSONSchema = { + type: 'object', + properties: { + paymentMethod: { + type: 'string', + title: 'Payment Method', + enum: ['credit_card', 'bank_transfer', 'paypal'] + } + }, + oneOf: [ + { + title: 'Credit Card', + properties: { + paymentMethod: { const: 'credit_card' }, + cardNumber: { + type: 'string', + title: 'Card Number', + pattern: '^\\d{16}$' + }, + expiryDate: { + type: 'string', + title: 'Expiry (MM/YY)', + pattern: '^\\d{2}/\\d{2}$' + }, + cvv: { + type: 'string', + title: 'CVV', + pattern: '^\\d{3,4}$' + } + } + }, + { + title: 'Bank Transfer', + properties: { + paymentMethod: { const: 'bank_transfer' }, + accountNumber: { + type: 'string', + title: 'Account Number' + }, + routingNumber: { + type: 'string', + title: 'Routing Number', + pattern: '^\\d{9}$' + } + } + }, + { + title: 'PayPal', + properties: { + paymentMethod: { const: 'paypal' }, + email: { + type: 'string', + format: 'email', + title: 'PayPal Email' + } + } + } + ] +} +``` + +## Shipping Options + +```typescript +const schema: JSONSchema = { + type: 'object', + properties: { + shippingMethod: { + type: 'string', + title: 'Shipping Method', + enum: ['standard', 'express', 'pickup'], + 'x-enum-labels': { + standard: 'Standard Shipping (5-7 days)', + express: 'Express Shipping (1-2 days)', + pickup: 'In-Store Pickup' + } + } + }, + oneOf: [ + { + properties: { + shippingMethod: { const: 'standard' }, + address: { + type: 'object', + title: 'Shipping Address', + properties: { + street: { type: 'string', title: 'Street' }, + city: { type: 'string', title: 'City' }, + state: { type: 'string', title: 'State' }, + zip: { type: 'string', title: 'ZIP' } + } + } + } + }, + { + properties: { + shippingMethod: { const: 'express' }, + address: { + type: 'object', + title: 'Shipping Address', + properties: { + street: { type: 'string', title: 'Street' }, + city: { type: 'string', title: 'City' }, + state: { type: 'string', title: 'State' }, + zip: { type: 'string', title: 'ZIP' } + } + }, + phoneNumber: { + type: 'string', + title: 'Phone Number', + description: 'Required for express delivery' + } + } + }, + { + properties: { + shippingMethod: { const: 'pickup' }, + store: { + type: 'string', + title: 'Pickup Location', + enum: ['store1', 'store2', 'store3'], + 'x-enum-labels': { + store1: 'Downtown Location', + store2: 'Westside Mall', + store3: 'North Plaza' + } + }, + pickupDate: { + type: 'string', + format: 'date', + title: 'Pickup Date' + } + } + } + ] +} +``` + +## How It Works + +### The Discriminator Field + +The first field acts as the "discriminator" that controls which schema is active: + +```typescript +{ + accountType: { + type: 'string', + enum: ['personal', 'business'] + } +} +``` + +### Conditional Schemas + +Each schema in `oneOf` uses `const` to match the discriminator value: + +```typescript +oneOf: [ + { + properties: { + accountType: { const: 'personal' }, // Match this value + // ... personal fields + } + }, + { + properties: { + accountType: { const: 'business' }, // Match this value + // ... business fields + } + } +] +``` + +### Form Behavior + +1. User selects a value in the discriminator field +2. QuickForms finds matching `oneOf` schema +3. Only fields from that schema are rendered +4. Validation applies only to visible fields + +## Complex Example: Survey Form + +```typescript +const schema: JSONSchema = { + type: 'object', + properties: { + employmentStatus: { + type: 'string', + title: 'Employment Status', + enum: ['employed', 'self-employed', 'unemployed', 'student', 'retired'] + } + }, + oneOf: [ + { + title: 'Employed', + properties: { + employmentStatus: { const: 'employed' }, + employer: { type: 'string', title: 'Employer' }, + jobTitle: { type: 'string', title: 'Job Title' }, + yearsEmployed: { type: 'number', title: 'Years at Current Job', minimum: 0 }, + annualIncome: { + type: 'number', + title: 'Annual Income', + minimum: 0, + 'x-component-props': { + prefix: '$' + } + } + } + }, + { + title: 'Self-Employed', + properties: { + employmentStatus: { const: 'self-employed' }, + businessName: { type: 'string', title: 'Business Name' }, + businessType: { + type: 'string', + title: 'Business Type', + enum: ['sole-proprietor', 'llc', 'corporation', 'partnership'] + }, + yearsInBusiness: { type: 'number', title: 'Years in Business', minimum: 0 }, + estimatedIncome: { + type: 'number', + title: 'Estimated Annual Income', + minimum: 0 + } + } + }, + { + title: 'Unemployed', + properties: { + employmentStatus: { const: 'unemployed' }, + lastEmployer: { type: 'string', title: 'Last Employer' }, + unemployedSince: { + type: 'string', + format: 'date', + title: 'Unemployed Since' + }, + seekingWork: { + type: 'boolean', + title: 'Actively Seeking Work', + default: true + } + } + }, + { + title: 'Student', + properties: { + employmentStatus: { const: 'student' }, + school: { type: 'string', title: 'School/University' }, + degreeProgram: { type: 'string', title: 'Degree Program' }, + expectedGraduation: { + type: 'string', + format: 'date', + title: 'Expected Graduation' + }, + partTimeWork: { + type: 'boolean', + title: 'Work Part-Time', + default: false + } + } + }, + { + title: 'Retired', + properties: { + employmentStatus: { const: 'retired' }, + retirementYear: { + type: 'number', + title: 'Year of Retirement', + minimum: 1950, + maximum: new Date().getFullYear() + }, + lastOccupation: { type: 'string', title: 'Last Occupation' }, + pensionIncome: { + type: 'number', + title: 'Annual Pension/Retirement Income', + minimum: 0 + } + } + } + ] +} +``` + +## Tips + +1. **Clear Labels**: Use descriptive `title` in each `oneOf` schema +2. **Default Values**: Set a default for the discriminator field +3. **Enum Labels**: Use `x-enum-labels` for better UX +4. **Validation**: Each `oneOf` schema can have its own `required` array +5. **Nested Objects**: Conditional schemas can include nested objects and arrays + +## Next Steps + +- [Custom Validation](/guide/guide/examples/custom-validation) - Add custom validation logic +- [Complex Types](/guide/complex-types) - More about oneOf, anyOf, allOf +- [Schema Extensions](/guide/schema-extensions) - Custom schema properties diff --git a/docs/guide/examples/custom-validation.md b/docs/guide/examples/custom-validation.md new file mode 100644 index 0000000..e2c4abc --- /dev/null +++ b/docs/guide/examples/custom-validation.md @@ -0,0 +1,450 @@ +# Custom Validation Example + +Add your own validation logic beyond JSON Schema constraints. + +## Basic Custom Validator + +```vue + + + +``` + +## Async Validation + +```vue + + + +``` + +## Complex Business Logic + +```typescript +const options = { + validators: { + // Age validation with context + age: (value, allValues, context) => { + const minAge = context.minimumAge || 18 + if (value < minAge) { + return `Must be at least ${minAge} years old` + } + return true + }, + + // Credit card validation + cardNumber: (value) => { + // Luhn algorithm + const digits = value.replace(/\s/g, '').split('').reverse() + let sum = 0 + for (let i = 0; i < digits.length; i++) { + let digit = parseInt(digits[i]) + if (i % 2 === 1) { + digit *= 2 + if (digit > 9) digit -= 9 + } + sum += digit + } + return sum % 10 === 0 || 'Invalid credit card number' + }, + + // Date range validation + endDate: (value, allValues) => { + if (!value || !allValues.startDate) return true + + const start = new Date(allValues.startDate) + const end = new Date(value) + + if (end <= start) { + return 'End date must be after start date' + } + + return true + }, + + // File upload validation + file: (value) => { + if (!value) return true + + const maxSize = 5 * 1024 * 1024 // 5MB + if (value.size > maxSize) { + return 'File size must be less than 5MB' + } + + const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'] + if (!allowedTypes.includes(value.type)) { + return 'File must be JPEG, PNG, or PDF' + } + + return true + } + }, + // Provide context + context: { + minimumAge: 21 + } +} +``` + +## Validation Return Types + +Validators can return different types: + +```typescript +const validators = { + field1: (value) => { + // Boolean + return true // Valid + return false // Invalid (generic error) + }, + + field2: (value) => { + // String error message + return 'Custom error message' + return true // Valid + }, + + field3: async (value) => { + // Promise + const result = await validateAsync(value) + return result.isValid || result.errorMessage + }, + + field4: (value) => { + // Validation result object + return { + valid: false, + message: 'Custom error message' + } + } +} +``` + +## Error Messages Priority + +QuickForms checks error messages in this order: + +1. **Custom validator errors** (highest priority) +2. **`x-error-messages` in schema** +3. **`errorMessages` in options** +4. **Default error messages** (lowest priority) + +```vue + +``` + +## Validation Modes + +Control when/how validation errors are displayed: + +```typescript +const options = { + // ValidateAndShow - Show errors as you type (default) + validationMode: 'ValidateAndShow', + + // ValidateAndHide - Validate silently, don't show errors + // validationMode: 'ValidateAndHide', + + // NoValidation - Skip all validation + // validationMode: 'NoValidation', + + validators: { + email: async (value) => { + // Validation still runs in all modes + // But errors only shown in ValidateAndShow + return await checkEmail(value) + } + } +} +``` + +## Complete Example: Registration Form + +```vue + + + +``` + +## Tips + +1. **Return boolean `true` for valid** - Don't return a success message +2. **Be specific with errors** - Clear error messages improve UX +3. **Debounce async validators** - Prevent excessive API calls +4. **Access all values** - Use second parameter for cross-field validation +5. **Use context** - Pass dynamic configuration via context object +6. **Handle async errors** - Wrap async code in try/catch + +## Next Steps + +- [Custom Validators Guide](/guide/custom-validators) - Deep dive into validators +- [Validation Guide](/guide/validation) - JSON Schema validation +- [Form Options](/guide/form-options) - All form options diff --git a/docs/guide/examples/nested-objects.md b/docs/guide/examples/nested-objects.md new file mode 100644 index 0000000..ca5e2e4 --- /dev/null +++ b/docs/guide/examples/nested-objects.md @@ -0,0 +1,356 @@ +# Nested Objects Example + +Working with nested object structures in QuickForms. + +## Complete Example + +```vue + + + + + +``` + +## Quasar Version + +With Quasar, nested objects are rendered in expandable cards: + +```vue + + + +``` + +## Key Concepts + +### Nested Structure + +The form data mirrors the schema structure: + +```typescript +{ + personalInfo: { + firstName: 'John', + lastName: 'Doe', + dateOfBirth: '1990-01-01' + }, + address: { + street: '123 Main St', + city: 'San Francisco', + state: 'CA', + zipCode: '94105' + }, + employment: { + company: 'Acme Corp', + position: 'Developer', + salary: 100000 + } +} +``` + +### Validation Paths + +VeeValidate handles nested paths automatically: +- `personalInfo.firstName` +- `address.zipCode` +- `employment.salary` + +### Required Nested Objects + +```typescript +required: ['personalInfo', 'address'] +``` + +This makes the entire nested object required. To make individual fields optional within a required object: + +```typescript +{ + address: { + type: 'object', + properties: { + street: { type: 'string' }, // Required + apt: { type: 'string' } // Optional + }, + required: ['street'] // Only street is required + } +} +``` + +### Custom Error Messages + +Use `x-error-messages` for better UX: + +```typescript +{ + zipCode: { + type: 'string', + pattern: '^\\d{5}$', + 'x-error-messages': { + pattern: 'Must be a 5-digit ZIP code', + required: 'ZIP code is required' + } + } +} +``` + +## Deeply Nested Example + +You can nest objects as deep as needed: + +```typescript +const schema: JSONSchema = { + type: 'object', + properties: { + company: { + type: 'object', + title: 'Company', + properties: { + name: { type: 'string', title: 'Company Name' }, + headquarters: { + type: 'object', + title: 'Headquarters', + properties: { + address: { + type: 'object', + title: 'Address', + properties: { + street: { type: 'string', title: 'Street' }, + coordinates: { + type: 'object', + title: 'GPS Coordinates', + properties: { + lat: { type: 'number', title: 'Latitude' }, + lng: { type: 'number', title: 'Longitude' } + } + } + } + } + } + } + } + } + } +} + +// Resulting data structure: +// { +// company: { +// name: '...', +// headquarters: { +// address: { +// street: '...', +// coordinates: { +// lat: 37.7749, +// lng: -122.4194 +// } +// } +// } +// } +// } +``` + +## Accessing Nested Data + +```vue + +``` + +## Tips + +1. **Grouping**: Use nested objects to logically group related fields +2. **Validation**: Each nested object can have its own `required` array +3. **UI Grouping**: Quasar automatically creates expandable sections for nested objects +4. **Deep Access**: Use optional chaining (`?.`) when accessing nested values +5. **Prefilling**: Set entire nested structures at once + +## Next Steps + +- [Arrays](/guide/guide/examples/arrays) - Dynamic lists of items +- [Conditional Fields](/guide/guide/examples/conditional-fields) - Fields that depend on other fields +- [Complex Types](/guide/complex-types) - Advanced schema features diff --git a/docs/guide/examples/theming.md b/docs/guide/examples/theming.md new file mode 100644 index 0000000..a8da39f --- /dev/null +++ b/docs/guide/examples/theming.md @@ -0,0 +1,482 @@ +# Theming Examples + +Customize the look and feel of your forms. + +## Plain Vue: CSS Variables + +QuickForms uses CSS custom properties for comprehensive theming. + +### Basic Theme Customization + +```vue + + + +``` + +### Dark Mode Theme + +```vue + + + + + +``` + +### Complete Custom Theme + +```vue + + + +``` + +## Quasar: Component Defaults + +For Quasar, use `componentDefaults` for consistent styling. + +### Global Quasar Defaults + +```vue + + + +``` + +### Per-Field Quasar Styling + +```typescript +const schema = { + type: 'object', + properties: { + name: { + type: 'string', + title: 'Full Name', + // Pass native Quasar props + 'x-quasar-props': { + outlined: true, + color: 'primary', + dense: true, + clearable: true, + hint: 'Enter your full legal name' + } + }, + priority: { + type: 'string', + enum: ['low', 'medium', 'high'], + 'x-quasar-props': { + outlined: true, + color: 'secondary', + useChips: false, + optionsDense: true + } + }, + description: { + type: 'string', + format: 'textarea', + 'x-quasar-props': { + outlined: true, + rows: 5, + autogrow: true, + counter: true, + maxlength: 500 + } + } + } +} +``` + +### Quasar Icons and Colors + +```typescript +const schema = { + type: 'object', + properties: { + email: { + type: 'string', + format: 'email', + title: 'Email', + // QuickForms convenience features + 'x-quickforms-quasar': { + prependIcon: 'mail', + iconColor: 'primary', + iconSize: 'md' + } + }, + username: { + type: 'string', + title: 'Username', + 'x-quickforms-quasar': { + prependIcon: 'person', + appendIcon: 'verified', // Shows on right + iconColor: 'positive' + } + }, + url: { + type: 'string', + format: 'url', + 'x-quickforms-quasar': { + prependIcon: 'link', + iconColor: 'info', + iconSize: 'sm' + } + } + } +} +``` + +### Quasar Array Buttons + +```typescript +const schema = { + type: 'object', + properties: { + tasks: { + type: 'array', + title: 'Tasks', + items: { + type: 'object', + properties: { + task: { type: 'string', title: 'Task' }, + done: { type: 'boolean', title: 'Done' } + } + }, + // Customize array buttons + 'x-quickforms-quasar': { + addButtonPosition: 'top-right', + addButton: { + label: 'Add Task', + icon: 'add_circle_outline', + color: 'positive', + push: true, + size: 'md' + }, + removeButton: { + icon: 'delete_outline', + color: 'negative', + flat: true, + round: true, + size: 'sm' + } + } + } + } +} +``` + +## Combining Themes with x-* Extensions + +Use schema extensions for per-field customization: + +```typescript +const schema = { + type: 'object', + properties: { + urgentField: { + type: 'string', + title: 'Urgent Task', + // Custom error messages + 'x-error-messages': { + required: 'This field is critical!' + }, + // Quasar styling + 'x-quasar-props': { + outlined: true, + color: 'negative', // Red theme + labelColor: 'negative' + }, + // QuickForms icons + 'x-quickforms-quasar': { + prependIcon: 'warning', + iconColor: 'negative' + } + }, + successField: { + type: 'string', + title: 'Completed', + 'x-quasar-props': { + outlined: true, + color: 'positive', // Green theme + readonly: true + }, + 'x-quickforms-quasar': { + prependIcon: 'check_circle', + iconColor: 'positive' + } + } + } +} +``` + +## Brand Color Examples + +### Professional Blue + +```css +:root { + --quickform-color-primary: #2563eb; + --quickform-color-primary-hover: #1d4ed8; + --quickform-color-border-focus: #3b82f6; +} +``` + +### Modern Purple + +```css +:root { + --quickform-color-primary: #8b5cf6; + --quickform-color-primary-hover: #7c3aed; + --quickform-color-border-focus: #a78bfa; +} +``` + +### Vibrant Orange + +```css +:root { + --quickform-color-primary: #f97316; + --quickform-color-primary-hover: #ea580c; + --quickform-color-border-focus: #fb923c; +} +``` + +### Fresh Green + +```css +:root { + --quickform-color-primary: #10b981; + --quickform-color-primary-hover: #059669; + --quickform-color-border-focus: #34d399; +} +``` + +## Minimal/Borderless Theme + +```css +.minimal-theme { + --quickform-color-border: transparent; + --quickform-color-bg-input: #f9fafb; + --quickform-radius-md: 0.5rem; + --quickform-shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); +} + +.minimal-theme input:focus, +.minimal-theme select:focus, +.minimal-theme textarea:focus { + --quickform-color-border: var(--quickform-color-primary); + box-shadow: 0 0 0 3px var(--quickform-color-primary-light); +} +``` + +## Material Design Theme + +```css +.material-theme { + /* Material colors */ + --quickform-color-primary: #1976d2; + --quickform-color-error: #d32f2f; + --quickform-color-success: #388e3c; + + /* Material elevation */ + --quickform-shadow-md: 0 2px 4px -1px rgba(0,0,0,.2), + 0 4px 5px 0 rgba(0,0,0,.14), + 0 1px 10px 0 rgba(0,0,0,.12); + + /* Material typography */ + --quickform-font-family: 'Roboto', sans-serif; + + /* Flatter borders */ + --quickform-radius-md: 4px; +} +``` + +## Tips + +1. **Start with defaults** - Override only what you need +2. **Use CSS variables** - Easy to maintain and switch themes +3. **Test dark mode** - Ensure sufficient contrast +4. **Consistent spacing** - Use spacing variables for harmony +5. **Brand colors** - Match your app's design system +6. **Accessibility** - Maintain WCAG contrast ratios + +## All Available CSS Variables + +For a complete list of CSS variables, see [STYLING_GUIDE.md](https://github.com/quickflo/quickforms/blob/main/STYLING_GUIDE.md) in the repository. + +## Next Steps + +- [Theming Guide](/guide/theming) - Complete theming documentation +- [Quasar Package](/guide/quasar) - Quasar-specific options +- [Schema Extensions](/guide/schema-extensions) - Custom properties diff --git a/docs/guide/field-types.md b/docs/guide/field-types.md new file mode 100644 index 0000000..747d234 --- /dev/null +++ b/docs/guide/field-types.md @@ -0,0 +1,72 @@ +# Field Types + +Complete reference for all supported field types in QuickForms. + +## Primitive Types + +### String +Basic text input. + +```typescript +{ type: 'string', title: 'Name' } +``` + +### String Formats +- `email` - Email input with validation +- `url` / `uri` - URL input with validation +- `date` - Date picker +- `time` - Time picker +- `date-time` - Date and time picker +- `password` - Password input with show/hide toggle +- `textarea` - Multi-line text area + +### Number / Integer +Numeric input with validation. + +```typescript +{ + type: 'number', + minimum: 0, + maximum: 100, + multipleOf: 0.5 +} +``` + +### Boolean +Checkbox input. + +```typescript +{ type: 'boolean', title: 'Accept terms' } +``` + +### Enum +Select dropdown. + +```typescript +{ + type: 'string', + enum: ['option1', 'option2', 'option3'], + 'x-enum-labels': { + 'option1': 'Option 1', + 'option2': 'Option 2', + 'option3': 'Option 3' + } +} +``` + +## Complex Types + +### Object +See [Complex Types](/guide/complex-types) for nested objects. + +### Array +See [Complex Types](/guide/complex-types) for arrays. + +### oneOf / anyOf / allOf +See [Complex Types](/guide/complex-types) for conditional schemas. + +## Next Steps + +- [Schema Basics](/guide/schema-basics) - Learn schema fundamentals +- [Complex Types](/guide/complex-types) - Work with nested structures +- [Examples](/guide/examples/basic-form) - See field types in action diff --git a/docs/guide/form-options.md b/docs/guide/form-options.md new file mode 100644 index 0000000..ed96976 --- /dev/null +++ b/docs/guide/form-options.md @@ -0,0 +1,357 @@ +# Form Options API + +Complete reference for the `options` prop on `DynamicForm`. + +## Interface + +```typescript +interface FormOptions { + useDefaults?: boolean + readonly?: boolean + disabled?: boolean + validationMode?: ValidationMode + errorMessages?: ErrorMessages + validators?: ValidatorMap + validatorDebounce?: number | Record + context?: Record + registry?: ComponentRegistry + labels?: FormLabels + componentDefaults?: ComponentDefaults +} +``` + +## Properties + +### `useDefaults` + +- **Type:** `boolean` +- **Default:** `true` + +Populate form with default values from schema's `default` properties. + +```vue + +``` + +### `readonly` + +- **Type:** `boolean` +- **Default:** `false` + +Make the entire form read-only. Fields are visible but not editable. + +```vue + +``` + +### `disabled` + +- **Type:** `boolean` +- **Default:** `false` + +Disable the entire form. Fields are visible but grayed out and not interactive. + +```vue + +``` + +### `validationMode` + +- **Type:** `'ValidateAndShow' | 'ValidateAndHide' | 'NoValidation'` +- **Default:** `'ValidateAndShow'` + +Control validation behavior: + +- **`ValidateAndShow`** - Validates as you type and displays errors +- **`ValidateAndHide`** - Validates but hides errors from user (prevents invalid submission) +- **`NoValidation`** - Completely disables validation + +```vue + +``` + +### `errorMessages` + +- **Type:** `Record>` +- **Default:** `{}` + +Override validation error messages per field and rule. + +```typescript +{ + errorMessages: { + 'email': { + required: 'Email is required', + format: 'Please enter a valid email address' + }, + 'password': { + minLength: 'Password must be at least 8 characters' + } + } +} +``` + +**Rule keys:** `required`, `minLength`, `maxLength`, `minimum`, `maximum`, `pattern`, `format`, `minItems`, `maxItems`, `uniqueItems` + +### `validators` + +- **Type:** `Record` +- **Default:** `{}` + +Add custom validation logic beyond JSON Schema capabilities. + +```typescript +type ValidatorFunction = ( + value: any, + allValues: Record, + context?: Record +) => boolean | string | Promise +``` + +**Sync validator:** +```typescript +{ + validators: { + confirmPassword: (value, allValues) => { + return value === allValues.password || 'Passwords must match' + } + } +} +``` + +**Async validator:** +```typescript +{ + validators: { + username: async (value) => { + const available = await checkUsername(value) + return available || 'Username taken' + } + } +} +``` + +### `validatorDebounce` + +- **Type:** `number | Record` +- **Default:** `300` + +Debounce delay in milliseconds for async validators. + +**Global debounce:** +```typescript +{ validatorDebounce: 500 } +``` + +**Per-field debounce:** +```typescript +{ + validatorDebounce: { + username: 500, + email: 300 + } +} +``` + +### `context` + +- **Type:** `Record` +- **Default:** `{}` + +Application context passed to validators and accessible in custom components. + +```typescript +{ + context: { + roles: ['admin', 'user'], + userId: 123, + tenant: 'acme-corp' + } +} +``` + +Used for role-based access control: + +```typescript +const schema = { + properties: { + adminField: { + type: 'string', + 'x-roles': { + admin: ['view', 'edit'], + user: [] + } + } + } +} +``` + +### `registry` + +- **Type:** `ComponentRegistry` +- **Default:** Default registry with built-in components + +Custom component registry for overriding or adding field renderers. + +```typescript +import { createDefaultRegistry } from '@quickflo/quickforms-vue' + +const registry = createDefaultRegistry() +registry.register('custom-phone', PhoneInput, tester) + +// Use in form + +``` + +See [Testers & Registry API](/guide/testers-registry) for details. + +### `labels` + +- **Type:** `FormLabels` +- **Default:** English labels + +Customize UI text for internationalization. + +```typescript +interface FormLabels { + selectPlaceholder?: string // "Select an option..." + addItem?: string // "Add item" + removeItem?: string // "Remove" + submit?: string // "Submit" + showPassword?: string // "Show password" + hidePassword?: string // "Hide password" +} +``` + +**Example:** +```typescript +{ + labels: { + selectPlaceholder: 'Seleccionar una opción...', + addItem: 'Agregar elemento', + submit: 'Enviar' + } +} +``` + +### `componentDefaults` + +- **Type:** `ComponentDefaults` +- **Default:** Component-specific defaults + +Configure default behavior for all components of a given type. + +```typescript +interface ComponentDefaults { + select?: { + autocomplete?: boolean + autocompleteThreshold?: number + } + array?: { + collapsible?: boolean + defaultCollapsed?: boolean + } + number?: { + prefix?: string + suffix?: string + } +} +``` + +**Example:** +```typescript +{ + componentDefaults: { + select: { + autocomplete: true, + autocompleteThreshold: 10 + }, + number: { + prefix: '$' + } + } +} +``` + +Override per-field using `x-component-props`: + +```typescript +{ + type: 'string', + enum: ['option1', 'option2'], + 'x-component-props': { + autocomplete: false // Override default + } +} +``` + +## Complete Example + +```vue + + + +``` diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md new file mode 100644 index 0000000..779f9c2 --- /dev/null +++ b/docs/guide/getting-started.md @@ -0,0 +1,297 @@ +# Getting Started + +Get up and running with QuickForms in minutes. + +## Installation + +::: code-group + +```sh [pnpm] +pnpm add @quickflo/quickforms @quickflo/quickforms-vue @quickflo/quickforms-quasar +``` + +```sh [npm] +npm install @quickflo/quickforms @quickflo/quickforms-vue @quickflo/quickforms-quasar +``` + +```sh [yarn] +yarn add @quickflo/quickforms @quickflo/quickforms-vue @quickflo/quickforms-quasar +``` + +::: + +::: info Plain Vue Users +If you're using plain Vue instead of Quasar, you only need `@quickflo/quickforms` and `@quickflo/quickforms-vue`. +::: + +## Your First Form + +::: tip +Create a simple contact form with name, email, and message fields. +::: +### Quasar + +::: tip Best Practice +For Quasar projects, create a boot file to configure QuickForms once and use it throughout your app. +::: + +**1. Create `src/boot/quickforms.ts`:** + +```typescript +import { boot } from 'quasar/wrappers' +import { createQuasarRegistry, QuasarFormOptions } from '@quickflo/quickforms-quasar' + +export const registry = createQuasarRegistry() + +export const options: QuasarFormOptions = { + registry, + componentDefaults: { + global: { + outlined: true, + dense: true, + }, + select: { + outlined: true, + }, + }, +} + +export default boot(({ app }) => { + console.log('QuickForms boot executed') + // Register custom components here if needed +}) +``` + +**2. Register the boot file in `quasar.config.js`:** + +```javascript +boot: [ + 'quickforms' +] +``` + +**3. Use in your components:** + +```vue + + + +``` + +This approach centralizes your QuickForms configuration and makes it easy to register custom components in one place. + +See [Quasar Package Docs](/guide/quasar) for all Quasar-specific options like `componentDefaults`, `x-quasar-props`, and `x-quickforms-quasar` features. + +### Plain Vue + +```vue + + + +``` + +See [Vue Package Docs](/guide/vue) for plain Vue component options. + +## What QuickForms Does + +QuickForms will: +- ✅ Render appropriate input types for each field +- ✅ Show email validation for the email field +- ✅ Mark required fields with asterisks +- ✅ Display validation errors as you type +- ✅ Two-way bind data via `v-model` + +## Understanding the Schema + +The `schema` object follows [JSON Schema Draft 7+](https://json-schema.org/) specification: + +```typescript +{ + type: 'object', // Root must be an object + properties: { // Define form fields + name: { + type: 'string', // Field type + title: 'Full Name', // Display label + minLength: 2 // Validation rule + } + }, + required: ['name'] // Required fields array +} +``` + +## Form Data Binding + +QuickForms uses `v-model` for two-way data binding: + +```vue + + + +``` + +## Form Options + +Customize form behavior with the `options` prop: + +```vue + +``` + +**See Complete Options:** +- [Form Options API](/guide/form-options) - Base options (validation, labels, etc.) +- [Quasar Package Options](/guide/quasar#configuration-options) - Quasar-specific (`componentDefaults`, `x-quasar-props`, etc.) +- [Vue Package Options](/guide/vue#components) - Plain Vue component options + +### Validation Modes + +- **`ValidateAndShow`** (default) - Validates as you type and shows errors +- **`ValidateAndHide`** - Validates silently, prevents invalid submission but hides errors +- **`NoValidation`** - Disables all validation + +```vue + +``` + +## Events + +QuickForms emits events for monitoring form state: + +```vue + + + +``` + +**Available Events:** +- `@update:modelValue` - Emitted when form data changes (automatic with `v-model`) +- `@validation` - Emitted when validation state changes + +QuickForms uses CSS custom properties for theming. Override them globally: + +```css +:root { + --quickform-color-primary: #8b5cf6; + --quickform-color-error: #ef4444; + --quickform-radius-md: 0.75rem; + --quickform-spacing-md: 1rem; +} +``` + +Or scope to a specific form: + +```vue + + + +``` + +See the [Theming Guide](/guide/theming) for complete customization options. + +## Next Steps + +- [Schema Basics](/guide/schema-basics) - Deep dive into JSON Schema +- [Field Types](/guide/field-types) - Learn about all supported field types +- [Validation](/guide/validation) - Advanced validation techniques +- [Examples](/guide/examples/basic-form) - See more complete examples diff --git a/docs/guide/i18n.md b/docs/guide/i18n.md new file mode 100644 index 0000000..72e01a1 --- /dev/null +++ b/docs/guide/i18n.md @@ -0,0 +1,198 @@ +# Internationalization + +Customize all UI text for internationalization or branding. + +## Available Labels + +QuickForms provides customizable labels for all UI text: + +```typescript +interface FormLabels { + selectPlaceholder?: string // Default: "Select an option..." + addItem?: string // Default: "Add item" + removeItem?: string // Default: "Remove" + submit?: string // Default: "Submit" + showPassword?: string // Default: "Show password" + hidePassword?: string // Default: "Hide password" +} +``` + +## Basic Example + +```vue + + + +``` + +## With Vue i18n + +Integrate with Vue's i18n plugin: + +```vue + + + +``` + +## Reactive Labels + +Labels can be reactive: + +```vue + + + +``` + +## Field Titles and Descriptions + +Field-level text comes from the schema: + +```typescript +const schema = { + type: 'object', + properties: { + name: { + type: 'string', + title: 'Nombre completo', // Spanish title + description: 'Ingrese su nombre' // Spanish description + } + } +} +``` + +## Error Messages + +Customize validation error messages per language: + +```typescript +const spanishOptions = { + labels: spanishLabels, + errorMessages: { + email: { + required: 'El correo electrónico es obligatorio', + format: 'Formato de correo electrónico no válido' + }, + password: { + minLength: 'La contraseña debe tener al menos 8 caracteres' + } + } +} +``` + +## Global Configuration + +For multiple forms, create a reusable configuration: + +```typescript +// i18n/quickforms.ts +export const quickformsI18n = { + en: { + selectPlaceholder: 'Select an option...', + addItem: 'Add item', + removeItem: 'Remove', + submit: 'Submit', + showPassword: 'Show password', + hidePassword: 'Hide password' + }, + es: { + selectPlaceholder: 'Seleccionar una opción...', + addItem: 'Agregar elemento', + removeItem: 'Eliminar', + submit: 'Enviar', + showPassword: 'Mostrar contraseña', + hidePassword: 'Ocultar contraseña' + }, + fr: { + selectPlaceholder: 'Sélectionner une option...', + addItem: 'Ajouter un élément', + removeItem: 'Supprimer', + submit: 'Soumettre', + showPassword: 'Afficher le mot de passe', + hidePassword: 'Masquer le mot de passe' + } +} + +// Usage +import { quickformsI18n } from './i18n/quickforms' + +const labels = quickformsI18n[currentLocale.value] +``` + +## Quasar Integration + +For Quasar users, labels integrate with Quasar's i18n: + +```typescript +import { useQuasar } from 'quasar' + +const $q = useQuasar() + +const labels = computed(() => quickformsI18n[$q.lang.getLocale()]) +``` + +## Next Steps + +- [Form Options API](/guide/form-options) - Complete labels reference +- [Validation](/guide/validation) - Custom error messages diff --git a/docs/guide/quasar.md b/docs/guide/quasar.md new file mode 100644 index 0000000..5756f60 --- /dev/null +++ b/docs/guide/quasar.md @@ -0,0 +1,304 @@ +# @quickflo/quickforms-quasar + +Quasar UI components for QuickForms with beautiful, pre-styled form fields. + +## Installation + +::: code-group + +```sh [pnpm] +pnpm add @quickflo/quickforms @quickflo/quickforms-vue @quickflo/quickforms-quasar +``` + +```sh [npm] +npm install @quickflo/quickforms @quickflo/quickforms-vue @quickflo/quickforms-quasar +``` + +```sh [yarn] +yarn add @quickflo/quickforms @quickflo/quickforms-vue @quickflo/quickforms-quasar +``` + +::: + + +## Quick Start + +```vue + + + +``` + +## Components + +The Quasar package provides pre-built Quasar-wrapped components: + +- **QuasarStringField** - `QInput` for text, email, URL, password, textarea +- **QuasarNumberField** - `QInput` with `type="number"` +- **QuasarBooleanField** - `QCheckbox` +- **QuasarEnumField** - `QSelect` for enums +- **QuasarDateField** - `QInput` with `QDate` popup +- **QuasarTimeField** - `QInput` with `QTime` popup +- **QuasarDateTimeField** - `QInput` with `QDate` and `QTime` popups +- **QuasarObjectField** - `QExpansionItem` for nested objects +- **QuasarArrayField** - `QCard` with add/remove buttons +- **QuasarOneOfField** - `QSelect` for conditional schemas + +## Configuration Options + +### Global Defaults + +Set defaults that apply to all components via `componentDefaults`. The values available are just a passthrough of the respective Quasar component's props. For example, the `input` accepts any valid property from `QInputProps`: + +```typescript +import { createQuasarRegistry } from '@quickflo/quickforms-quasar' +import type { QuasarFormOptions } from '@quickflo/quickforms-quasar' + +const registry = createQuasarRegistry() + +const options: QuasarFormOptions = { + registry, + componentDefaults: { + global: { + outlined: true, // All components use outlined style + dense: true, // All components use dense mode + color: 'primary' // All components use primary color + }, + input: { + clearable: true // All text inputs get clear button + }, + select: { + useChips: true // Enum fields display as chips + }, + checkbox: { + color: 'secondary' + } + } +} +``` + +### Per-Field Overrides + +Use `x-quasar-props` to pass native Quasar component props. Similar to the componentDefaults in that the properties available are just a passthrough of the respective Quasar component: + +```typescript +{ + type: 'string', + format: 'textarea', + title: 'Description', + 'x-quasar-props': { + rows: 10, + dense: true, + outlined: true, + color: 'primary' + } +} +``` + +### QuickForms Convenience Features + +Use `x-quickforms-quasar` for convenience features (not native Quasar props): + +#### Icons + +```typescript +{ + type: 'string', + title: 'Email', + 'x-quickforms-quasar': { + prependIcon: 'mail', + iconColor: 'primary', + iconSize: 'md' + } +} +``` + +**Icon Properties:** +- `prependIcon` - Icon on left side +- `appendIcon` - Icon on right side (not available for password/select) +- `iconColor` - Quasar color (default: `'grey-7'`) +- `iconSize` - Size: `'xs'`, `'sm'`, `'md'`, `'lg'`, `'xl'` (default: `'sm'`) + +#### Array Buttons + +Customize array field buttons with native QBtn props: + +```typescript +{ + type: 'array', + items: { type: 'string' }, + title: 'Tags', + 'x-quickforms-quasar': { + addButtonPosition: 'top-right', + addButton: { + label: 'Add Tag', + icon: 'add_circle', + color: 'secondary', + size: 'md' + }, + removeButton: { + icon: 'delete', + color: 'negative' + } + } +} +``` + +**Array Properties:** +- `addButtonPosition` - Position: `'top-left'`, `'top-right'`, `'bottom-left'`, `'bottom-right'` +- `addButton` - Native QBtn props (label, icon, color, size, push, fab, etc.) +- `removeButton` - Native QBtn props + +## Supported Formats + +QuickForms Quasar supports all standard JSON Schema formats: + +### Standard Formats (with validation) +- `email` - Email validation (RFC 5321) +- `url` / `uri` - URL validation (RFC 3986) +- `date` - Date picker (YYYY-MM-DD) +- `time` - Time picker +- `date-time` - Date and time picker + +### Custom UI Hints (no validation) +- `password` - Password input with show/hide toggle +- `textarea` - Multi-line text area + +## DateTime Customization + +Datetime fields default to 12-hour AM/PM format. Customize per-field: + +```typescript +{ + type: 'string', + format: 'date-time', + title: 'Event Time', + 'x-quasar-props': { + format24h: true, + withSeconds: true, + mask: 'YYYY-MM-DD HH:mm:ss' + } +} +``` + +Or globally: + +```typescript +{ + componentDefaults: { + datetime: { + format24h: true, + dateMask: 'MM/DD/YYYY', + timeMask: 'HH:mm' + } + } +} +``` + +::: warning +The default 12-hour format (`YYYY-MM-DD hh:mm A`) won't pass JSON Schema `format: "date-time"` validation, which requires ISO 8601. Transform to ISO 8601 before submission if needed. +::: + +## Theming + +Quasar components automatically inherit your app's theme. Options: + +1. **Quasar SASS Variables** - Customize `quasar.variables.sass` +2. **Component Defaults** - Use `componentDefaults.global` +3. **Dark Mode** - Automatic via Quasar's Dark plugin + +```typescript +const options: QuasarFormOptions = { + registry: createQuasarRegistry(), + componentDefaults: { + global: { + outlined: true, + dense: true, + color: 'primary' + } + } +} +``` + +## Complete Options Reference + +### `QuasarFormOptions` + +Extends standard `FormOptions` with Quasar-specific configurations: + +```typescript +interface QuasarFormOptions extends FormOptions { + registry: ComponentRegistry + componentDefaults?: { + global?: Record // Applied to ALL components + input?: Record // QInput defaults + select?: Record // QSelect defaults + checkbox?: Record // QCheckbox defaults + date?: Record // QDate defaults + time?: Record // QTime defaults + datetime?: Record // DateTime field defaults + } + quickformsDefaults?: { + input?: { + iconColor?: string + iconSize?: string + } + array?: { + addButtonPosition?: string + addButton?: Record + removeButton?: Record + } + } +} +``` + +For base `FormOptions`, see [Form Options API](/guide/form-options). + +## Examples + +See [Examples](/guide/examples/basic-form) for complete working examples with Quasar. + +## API Reference + +### `createQuasarRegistry()` + +Creates a component registry with all Quasar components registered. + +**Returns:** `ComponentRegistry` + +**Example:** +```typescript +import { createQuasarRegistry } from '@quickflo/quickforms-quasar' +const registry = createQuasarRegistry() +``` + +## Next Steps + +- [Getting Started](/guide/getting-started) - Complete tutorial +- [Form Options API](/guide/form-options) - Base options reference +- [Examples](/guide/examples/basic-form) - Working examples diff --git a/docs/guide/rbac.md b/docs/guide/rbac.md new file mode 100644 index 0000000..c3b0883 --- /dev/null +++ b/docs/guide/rbac.md @@ -0,0 +1,257 @@ +# Role-Based Access Control + +Control field visibility and editability based on user roles. + +## Basic Example + +```typescript +const schema = { + type: 'object', + properties: { + name: { + type: 'string', + title: 'Name' + // Visible to everyone + }, + email: { + type: 'string', + format: 'email', + title: 'Email' + // Visible to everyone + }, + salary: { + type: 'number', + title: 'Salary', + 'x-roles': { + admin: ['view', 'edit'], // Admins can see and edit + manager: ['view'], // Managers can only view + employee: [] // Employees cannot see + } + }, + apiKey: { + type: 'string', + title: 'API Key', + 'x-roles': { + admin: ['view', 'edit'], // Only admins have access + manager: [], + employee: [] + } + } + } +} +``` + +## Setting User Roles + +Pass the current user's roles via form options: + +```vue + + + +``` + +## Role Permissions + +Each role can have these permissions: + +- **`['view', 'edit']`** - Field is visible and editable +- **`['view']`** - Field is visible but read-only +- **`[]`** - Field is completely hidden + +## Multiple Roles + +Users can have multiple roles. QuickForms checks if the user has **any** role with the required permissions: + +```typescript +const options = { + context: { + roles: ['employee', 'team-lead'] // User has multiple roles + } +} + +// In schema +'x-roles': { + 'employee': ['view'], + 'team-lead': ['view', 'edit'], // This applies! + 'admin': ['view', 'edit'] +} +``` + +If a user has multiple roles with different permissions, the **highest permission level** applies. + +## Hide All Fields + +Use `x-hidden` to completely hide a field from everyone: + +```typescript +{ + systemId: { + type: 'string', + 'x-hidden': true // Never shown to any user + } +} +``` + +## Common Patterns + +### Admin-Only Fields + +```typescript +{ + adminSettings: { + type: 'object', + 'x-roles': { + admin: ['view', 'edit'], + user: [] + }, + properties: { + // ... admin settings + } + } +} +``` + +### Read-Only for Most Users + +```typescript +{ + createdBy: { + type: 'string', + title: 'Created By', + 'x-roles': { + admin: ['view', 'edit'], // Only admins can change + user: ['view'] // Everyone else can only view + } + } +} +``` + +### Progressive Disclosure + +```typescript +{ + basicInfo: { + type: 'string', + // No x-roles = visible to everyone + }, + advancedSettings: { + type: 'object', + 'x-roles': { + powerUser: ['view', 'edit'], + admin: ['view', 'edit'], + basic: [] + } + } +} +``` + +## Dynamic Roles from API + +Fetch user roles from your API: + +```vue + + + +``` + +## Conditional Logic with Roles + +Combine with custom validators for role-based validation: + +```typescript +const options = { + context: { + roles: ['user'] + }, + validators: { + budget: (value, allValues, context) => { + // Only admins can set budget over 10000 + if (value > 10000 && !context?.roles?.includes('admin')) { + return 'Only admins can set budget over $10,000' + } + return true + } + } +} +``` + +## Nested Objects + +Role controls apply to nested fields too: + +```typescript +{ + userProfile: { + type: 'object', + properties: { + publicInfo: { + type: 'string' + // Visible to all + }, + privateInfo: { + type: 'string', + 'x-roles': { + admin: ['view', 'edit'], + user: [] + } + } + } + } +} +``` + +## Best Practices + +1. **Always validate on the backend** - Client-side role checks are for UX only +2. **Use consistent role names** - Match your backend's role system +3. **Default to restrictive** - Fields without `x-roles` are visible to everyone +4. **Document role requirements** - Keep a list of all roles used in your schemas +5. **Test with different roles** - Verify the form looks correct for each role + +## Next Steps + +- [Form Options API](/guide/form-options) - Context options +- [Custom Validators](/guide/custom-validators) - Role-based validation +- [Schema Extensions](/guide/schema-extensions) - `x-roles` specification diff --git a/docs/guide/schema-basics.md b/docs/guide/schema-basics.md new file mode 100644 index 0000000..ac54f47 --- /dev/null +++ b/docs/guide/schema-basics.md @@ -0,0 +1,276 @@ +# Schema Basics + +QuickForms uses [JSON Schema](https://json-schema.org/) to define form structure and validation. This guide covers the fundamentals. + +## JSON Schema Introduction + +JSON Schema is an industry-standard vocabulary for validating and documenting JSON data. It's: +- **Self-documenting** - Schemas describe data structure and constraints +- **Language-agnostic** - Works across any programming language +- **Extensible** - Supports custom extensions via `x-*` attributes +- **Widely adopted** - Used by OpenAPI, VS Code, and many other tools + +## Basic Structure + +Every QuickForms schema starts with an object: + +```typescript +const schema: JSONSchema = { + type: 'object', + properties: { + // Field definitions go here + } +} +``` + +## Primitive Types + +### String + +```typescript +{ + fieldName: { + type: 'string', + title: 'Field Label', + minLength: 2, + maxLength: 50, + pattern: '^[A-Za-z]+$', // Regex validation + default: 'Default value' + } +} +``` + +**String Formats:** +- `email` - Email validation +- `url` / `uri` - URL validation +- `date` - Date picker (YYYY-MM-DD) +- `time` - Time picker (HH:mm:ss) +- `date-time` - Date+time picker +- `password` - Password input with show/hide toggle +- `textarea` - Multi-line text input + +```typescript +{ + email: { type: 'string', format: 'email' }, + website: { type: 'string', format: 'url' }, + birthdate: { type: 'string', format: 'date' }, + password: { type: 'string', format: 'password' }, + bio: { type: 'string', format: 'textarea' } +} +``` + +### Number / Integer + +```typescript +{ + age: { + type: 'number', // Use 'integer' for whole numbers only + title: 'Age', + minimum: 18, + maximum: 120, + multipleOf: 1, // Step increment + default: 25 + } +} +``` + +### Boolean + +```typescript +{ + newsletter: { + type: 'boolean', + title: 'Subscribe to newsletter', + default: false + } +} +``` + +Rendered as a checkbox. + +### Enum (Select) + +```typescript +{ + status: { + type: 'string', + enum: ['draft', 'active', 'archived'], + title: 'Status', + default: 'draft' + } +} +``` + +Rendered as a select dropdown. For better UX, add custom labels: + +```typescript +{ + status: { + type: 'string', + enum: ['draft', 'active', 'archived'], + 'x-enum-labels': { + draft: '📝 Draft', + active: '✅ Active', + archived: '📦 Archived' + } + } +} +``` + +## Common Properties + +### `title` +Display label for the field. If omitted, the property key is used. + +```typescript +{ type: 'string', title: 'Full Name' } // Shows "Full Name" +{ type: 'string' } // Shows "fieldName" +``` + +### `description` +Help text displayed below the field. + +```typescript +{ + password: { + type: 'string', + format: 'password', + description: 'Must be at least 8 characters' + } +} +``` + +For HTML hints, use `x-hint`: + +```typescript +{ + email: { + type: 'string', + format: 'email', + 'x-hint': 'We follow strict privacy rules' + } +} +``` + +### `default` +Default value when form is initialized. + +```typescript +{ + country: { + type: 'string', + enum: ['US', 'CA', 'UK'], + default: 'US' // Pre-selected + } +} +``` + +Enable `useDefaults` in form options to populate defaults: + +```vue + +``` + +### `required` +Required fields are specified at the parent object level: + +```typescript +{ + type: 'object', + properties: { + name: { type: 'string' }, + email: { type: 'string' } + }, + required: ['name', 'email'] // Both required +} +``` + +Required fields show an asterisk (*) in their label. + +## Validation Keywords + +### String Validation +- `minLength` / `maxLength` - Length constraints +- `pattern` - Regular expression (JavaScript flavor) +- `format` - Predefined formats (email, url, date, etc.) + +```typescript +{ + username: { + type: 'string', + minLength: 3, + maxLength: 20, + pattern: '^[a-zA-Z0-9_]+$' + } +} +``` + +### Number Validation +- `minimum` / `maximum` - Inclusive bounds +- `exclusiveMinimum` / `exclusiveMaximum` - Exclusive bounds +- `multipleOf` - Must be divisible by this value + +```typescript +{ + price: { + type: 'number', + minimum: 0, + maximum: 9999.99, + multipleOf: 0.01 // Two decimal places + } +} +``` + +## Complete Example + +```typescript +const schema: JSONSchema = { + type: 'object', + properties: { + // String field + name: { + type: 'string', + title: 'Full Name', + minLength: 2, + description: 'First and last name' + }, + + // Email field + email: { + type: 'string', + format: 'email', + title: 'Email Address' + }, + + // Number field + age: { + type: 'integer', + title: 'Age', + minimum: 18, + maximum: 120 + }, + + // Boolean field + terms: { + type: 'boolean', + title: 'I agree to the terms' + }, + + // Enum field + role: { + type: 'string', + enum: ['user', 'admin', 'moderator'], + title: 'Role', + default: 'user' + } + }, + required: ['name', 'email', 'age', 'terms'] +} +``` + +## Next Steps + +- [Field Types](/guide/field-types) - Deep dive into all field types +- [Validation](/guide/validation) - Advanced validation techniques +- [Complex Types](/guide/complex-types) - Objects, arrays, and conditionals +- [Schema Extensions](/guide/schema-extensions) - Custom `x-*` attributes diff --git a/docs/guide/schema-extensions.md b/docs/guide/schema-extensions.md new file mode 100644 index 0000000..5e9b99a --- /dev/null +++ b/docs/guide/schema-extensions.md @@ -0,0 +1,422 @@ +# Schema Extensions + +QuickForms extends JSON Schema with custom `x-*` attributes to provide escape hatches for common customization needs. + +::: tip +All `x-*` attributes are optional. QuickForms works perfectly with standard JSON Schema—use extensions only when you need them. +::: + +## `x-hidden` + +**Purpose:** Completely hide a field from rendering + +**Type:** `boolean` + +**Example:** +```typescript +{ + systemId: { + type: 'string', + 'x-hidden': true // Field is never shown + } +} +``` + +**Use Cases:** +- Hidden system fields +- Internal tracking IDs +- Fields set programmatically + +--- + +## `x-roles` + +**Purpose:** Role-based access control for field visibility and editability + +**Type:** `Record` + +**Example:** +```typescript +{ + salary: { + type: 'number', + title: 'Salary', + 'x-roles': { + admin: ['view', 'edit'], // Admins can see and edit + manager: ['view'], // Managers can only view + employee: [] // Employees cannot see + } + } +} +``` + +**Permissions:** +- `['view', 'edit']` - Field is visible and editable +- `['view']` - Field is visible but read-only +- `[]` - Field is completely hidden + +**Related:** [Role-Based Access Guide](/guide/rbac) + +--- + +## `x-enum-labels` + +**Purpose:** Custom display text for enum options while keeping underlying values + +**Type:** `Record` + +**Example:** +```typescript +{ + status: { + type: 'string', + enum: ['draft', 'active', 'archived'], + 'x-enum-labels': { + 'draft': '📝 Draft', + 'active': '✅ Active', + 'archived': '📦 Archived' + } + } +} +``` + +**Use Cases:** +- User-friendly labels for technical values +- Internationalization of enum options +- Adding icons/emojis to options +- Verbose descriptions for enum values + +--- + +## `x-item-label` + +**Purpose:** Custom labels for array items with template interpolation + +**Type:** `string | "none" | false` + +**Example:** +```typescript +{ + workHistory: { + type: 'array', + title: 'Work History', + 'x-item-label': '{{company}} - {{position}}', + items: { + type: 'object', + properties: { + company: { type: 'string', title: 'Company' }, + position: { type: 'string', title: 'Position' }, + years: { type: 'number', title: 'Years' } + } + } + } +} +``` + +**Template Variables:** +- Use `{{propertyName}}` to interpolate item properties +- Set to `"none"` or `false` to hide labels entirely + +--- + +## `x-error-messages` + +**Purpose:** Custom validation error messages per rule type + +**Type:** `Record` + +**Example:** +```typescript +{ + password: { + type: 'string', + minLength: 8, + 'x-error-messages': { + required: 'Password is required for security', + minLength: 'Password must be at least 8 characters long' + } + } +} +``` + +**Available Keys:** +- `required` - When field is required but empty +- `minLength` / `maxLength` - String length validation +- `minimum` / `maximum` - Number range validation +- `pattern` - Regex pattern validation +- `format` - Format validation (email, url, etc.) +- `minItems` / `maxItems` - Array length validation +- `uniqueItems` - Array uniqueness validation + +**Alternative:** Use `errorMessages` in form options for app-level messages + +--- + +## `x-component-props` + +**Purpose:** Override component-specific behavior for a single field + +**Type:** `Record` + +**Example:** + +**Plain Vue:** +```typescript +{ + country: { + type: 'string', + enum: ['US', 'CA', 'UK', /* ...100+ countries */], + 'x-component-props': { + autocomplete: true // Enable autocomplete for this select + } + } +} +``` + +**Quasar:** +```typescript +{ + bio: { + type: 'string', + format: 'textarea', + 'x-component-props': { + rows: 10, + dense: true, + outlined: true + } + } +} +``` + +**Use Cases:** +- Per-field component customization +- Override global component defaults +- Pass native component props + +--- + +## `x-quasar-props` + +**Purpose:** Pass native Quasar component props (Quasar package only) + +**Type:** `Record` + +**Example:** +```typescript +{ + priority: { + type: 'string', + enum: ['low', 'medium', 'high'], + 'x-quasar-props': { + color: 'secondary', + dense: true, + outlined: true, + clearable: true + } + } +} +``` + +**Note:** This is an alias for `x-component-props` but makes it clear you're using Quasar-specific props. + +**Related:** [Quasar Package Docs](/guide/quasar) + +--- + +## `x-quickforms-quasar` + +**Purpose:** QuickForms convenience features for Quasar (not native Quasar props) + +**Type:** `Record` + +**Example:** + +**Icons:** +```typescript +{ + email: { + type: 'string', + format: 'email', + 'x-quickforms-quasar': { + prependIcon: 'mail', + iconColor: 'primary', + iconSize: 'md' + } + } +} +``` + +**Array Buttons:** +```typescript +{ + tags: { + type: 'array', + items: { type: 'string' }, + 'x-quickforms-quasar': { + addButtonPosition: 'top-right', + addButton: { + label: 'Add Tag', + icon: 'add_circle', + color: 'secondary' + }, + removeButton: { + icon: 'delete', + color: 'negative' + } + } + } +} +``` + +**Available Properties:** + +**For Icons:** +- `prependIcon` - Icon on left side +- `appendIcon` - Icon on right side (not for password/select) +- `iconColor` - Quasar color +- `iconSize` - `'xs' | 'sm' | 'md' | 'lg' | 'xl'` + +**For Arrays:** +- `addButtonPosition` - `'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'` +- `addButton` - Native QBtn props +- `removeButton` - Native QBtn props + +**Related:** [Quasar Package Docs](/guide/quasar#quickforms-convenience-features) + +--- + +## `x-hint` + +**Purpose:** HTML-enabled hint text (takes precedence over `description`) + +**Type:** `string` + +**Example:** +```typescript +{ + email: { + type: 'string', + format: 'email', + 'x-hint': 'Read our privacy policy' + } +} +``` + +**Difference from `description`:** +- `description` - Plain text only +- `x-hint` - Supports HTML for links, formatting, etc. + +--- + +## `x-hint-mode` + +**Purpose:** Control when hints are displayed + +**Type:** `"always" | "focus" | "hover"` + +**Default:** `"always"` + +**Example:** +```typescript +{ + password: { + type: 'string', + format: 'password', + description: 'Must be 8+ characters', + 'x-hint-mode': 'focus' // Only show hint when field is focused + } +} +``` + +--- + +## Combining Extensions + +Multiple extensions can be used together: + +```typescript +{ + adminEmail: { + type: 'string', + format: 'email', + title: 'Admin Email', + + // Role-based access + 'x-roles': { + admin: ['view', 'edit'], + user: ['view'] + }, + + // Custom error messages + 'x-error-messages': { + required: 'Admin email is required', + format: 'Please enter a valid email address' + }, + + // Hint with HTML + 'x-hint': 'Will receive all notifications', + + // Quasar-specific + 'x-quickforms-quasar': { + prependIcon: 'admin_panel_settings', + iconColor: 'primary' + } + } +} +``` + +## Extension Priority + +When multiple sources provide the same configuration: + +1. **`x-*` attributes in schema** - Highest priority (field-specific) +2. **`options.componentDefaults`** - Component type defaults +3. **Built-in defaults** - Lowest priority + +Example: +```typescript +// Global default +componentDefaults: { + input: { clearable: true } +} + +// Field override +'x-component-props': { + clearable: false // This wins +} +``` + +## Best Practices + +1. **Use sparingly** - Extensions are escape hatches, not primary API +2. **Prefer standard JSON Schema** - Only use extensions when needed +3. **Document your extensions** - Comment why you're using them +4. **Validate your schema** - Extensions should not break standard JSON Schema validation +5. **Namespace custom extensions** - Use `x-yourapp-*` for app-specific extensions + +## Standard JSON Schema Properties + +Remember, many things don't need extensions: + +```typescript +{ + // Standard JSON Schema (use these first!) + title: 'Field Label', // Display label + description: 'Help text', // Plain text hint + default: 'default value', // Default value + examples: ['example1'], // Example values + enum: ['a', 'b', 'c'], // Allowed values + const: 'fixed-value', // Constant value + + // Only add x-* when you need more control + 'x-enum-labels': { ... }, // When enum values need friendly labels + 'x-hint': '...', // When you need HTML in hints + 'x-roles': { ... } // When you need access control +} +``` + +## Next Steps + +- [Form Options API](/guide/form-options) - Configure options at form level +- [Role-Based Access](/guide/rbac) - Using `x-roles` +- [Quasar Package](/guide/quasar) - Quasar-specific extensions diff --git a/docs/guide/testers-registry.md b/docs/guide/testers-registry.md new file mode 100644 index 0000000..3b77d14 --- /dev/null +++ b/docs/guide/testers-registry.md @@ -0,0 +1,600 @@ +# Testers & Registry API + +The component registry and tester system controls which component renders each field. + +## Overview + +QuickForms uses a **priority-based tester system** borrowed from JSONForms: + +1. For each field, all registered testers are evaluated +2. Each tester returns a priority number (higher = better match) +3. The component with the highest priority renders the field + +This allows you to: +- Override built-in components +- Add components for custom formats +- Create specialized components for specific use cases + +## Component Registry + +### createDefaultRegistry + +Creates a registry with all standard Vue components registered. + +```typescript +function createDefaultRegistry(): ComponentRegistry +``` + +**Example:** + +```typescript +import { createDefaultRegistry } from '@quickflo/quickforms-vue' + +const registry = createDefaultRegistry() +``` + +### createQuasarRegistry + +Creates a registry with all Quasar components registered. + +```typescript +function createQuasarRegistry(): ComponentRegistry +``` + +**Example:** + +```typescript +import { createQuasarRegistry } from '@quickflo/quickforms-quasar' + +const registry = createQuasarRegistry() +``` + +### ComponentRegistry Methods + +#### register + +Register a new component with its tester. + +```typescript +registry.register( + name: string, + component: Component, + tester: TesterFunction +): void +``` + +**Parameters:** +- `name` - Unique identifier for the component +- `component` - Vue component +- `tester` - Function that returns priority number + +**Example:** + +```typescript +import CustomInput from './CustomInput.vue' +import { rankWith, isStringType } from '@quickflo/quickforms-vue' + +registry.register( + 'custom-string', + CustomInput, + rankWith(10, isStringType) +) +``` + +#### getComponent + +Get the best-matching component for a schema. + +```typescript +registry.getComponent(schema: JSONSchema): Component | undefined +``` + +**Example:** + +```typescript +const schema = { type: 'string', format: 'email' } +const component = registry.getComponent(schema) +``` + +--- + +## Tester Functions + +Testers evaluate schemas and return priority numbers. + +### TesterFunction + +```typescript +type TesterFunction = (schema: JSONSchema) => number +``` + +Returns: +- `0` - Does not match +- `> 0` - Matches (higher = better match) + +### rankWith + +Combines a priority rank with a predicate function. + +```typescript +function rankWith( + rank: number, + predicate: (schema: JSONSchema) => boolean +): TesterFunction +``` + +**Example:** + +```typescript +import { rankWith, isStringType } from '@quickflo/quickforms-vue' + +// Priority 10 if schema is string type +const tester = rankWith(10, isStringType) + +console.log(tester({ type: 'string' })) // 10 +console.log(tester({ type: 'number' })) // 0 +``` + +--- + +## Type Testers + +Check the JSON Schema `type` property. + +### isStringType + +```typescript +function isStringType(schema: JSONSchema): boolean +``` + +Returns `true` if `schema.type === 'string'`. + +### isNumberType + +```typescript +function isNumberType(schema: JSONSchema): boolean +``` + +Returns `true` if `schema.type === 'number'`. + +### isIntegerType + +```typescript +function isIntegerType(schema: JSONSchema): boolean +``` + +Returns `true` if `schema.type === 'integer'`. + +### isBooleanType + +```typescript +function isBooleanType(schema: JSONSchema): boolean +``` + +Returns `true` if `schema.type === 'boolean'`. + +### isObjectType + +```typescript +function isObjectType(schema: JSONSchema): boolean +``` + +Returns `true` if `schema.type === 'object'`. + +### isArrayType + +```typescript +function isArrayType(schema: JSONSchema): boolean +``` + +Returns `true` if `schema.type === 'array'`. + +### isNullType + +```typescript +function isNullType(schema: JSONSchema): boolean +``` + +Returns `true` if `schema.type === 'null'`. + +### isEnumType + +```typescript +function isEnumType(schema: JSONSchema): boolean +``` + +Returns `true` if schema has `enum` property. + +--- + +## Format Testers + +Check the JSON Schema `format` property. + +### hasFormat + +```typescript +function hasFormat(format?: string): (schema: JSONSchema) => boolean +``` + +Returns predicate that checks if schema has a specific format. + +**Examples:** + +```typescript +import { hasFormat } from '@quickflo/quickforms-vue' + +// Check for specific format +const isEmail = hasFormat('email') +console.log(isEmail({ type: 'string', format: 'email' })) // true + +// Check for any format +const hasAnyFormat = hasFormat() +console.log(hasAnyFormat({ type: 'string', format: 'email' })) // true +console.log(hasAnyFormat({ type: 'string' })) // false +``` + +### isEmailFormat + +```typescript +function isEmailFormat(schema: JSONSchema): boolean +``` + +Returns `true` if `schema.format === 'email'`. + +### isDateFormat + +```typescript +function isDateFormat(schema: JSONSchema): boolean +``` + +Returns `true` if `schema.format === 'date'`. + +### isTimeFormat + +```typescript +function isTimeFormat(schema: JSONSchema): boolean +``` + +Returns `true` if `schema.format === 'time'`. + +### isDateTimeFormat + +```typescript +function isDateTimeFormat(schema: JSONSchema): boolean +``` + +Returns `true` if `schema.format === 'date-time'`. + +### isUrlFormat + +```typescript +function isUrlFormat(schema: JSONSchema): boolean +``` + +Returns `true` if `schema.format === 'url'` or `'uri'`. + +--- + +## Composition Testers + +Check for schema composition keywords. + +### hasOneOf + +```typescript +function hasOneOf(schema: JSONSchema): boolean +``` + +Returns `true` if schema has `oneOf` property. + +### hasAnyOf + +```typescript +function hasAnyOf(schema: JSONSchema): boolean +``` + +Returns `true` if schema has `anyOf` property. + +### hasAllOf + +```typescript +function hasAllOf(schema: JSONSchema): boolean +``` + +Returns `true` if schema has `allOf` property. + +### hasConst + +```typescript +function hasConst(schema: JSONSchema): boolean +``` + +Returns `true` if schema has `const` property. + +### hasConditional + +```typescript +function hasConditional(schema: JSONSchema): boolean +``` + +Returns `true` if schema has `if/then/else` conditional. + +### isDiscriminatedUnion + +```typescript +function isDiscriminatedUnion(schema: JSONSchema): boolean +``` + +Returns `true` if schema is a discriminated union (oneOf with discriminator). + +--- + +## Extension Testers + +Check for custom schema extensions. + +### hasExtension + +```typescript +function hasExtension( + key: string, + value?: any +): (schema: JSONSchema) => boolean +``` + +Returns predicate that checks for custom `x-*` properties. + +**Examples:** + +```typescript +import { hasExtension } from '@quickflo/quickforms-vue' + +// Check if extension exists +const hasWidget = hasExtension('x-widget') +console.log(hasWidget({ type: 'string', 'x-widget': 'color' })) // true + +// Check if extension has specific value +const isColorWidget = hasExtension('x-widget', 'color') +console.log(isColorWidget({ type: 'string', 'x-widget': 'color' })) // true +console.log(isColorWidget({ type: 'string', 'x-widget': 'slider' })) // false +``` + +--- + +## Logical Combinators + +Combine multiple testers with logical operators. + +### and + +```typescript +function and( + ...predicates: Array<(schema: JSONSchema) => boolean> +): (schema: JSONSchema) => boolean +``` + +Returns `true` if ALL predicates match. + +**Example:** + +```typescript +import { and, isStringType, hasFormat } from '@quickflo/quickforms-vue' + +// Must be string AND have email format +const isEmailString = and( + isStringType, + hasFormat('email') +) + +console.log(isEmailString({ type: 'string', format: 'email' })) // true +console.log(isEmailString({ type: 'string' })) // false +``` + +### or + +```typescript +function or( + ...predicates: Array<(schema: JSONSchema) => boolean> +): (schema: JSONSchema) => boolean +``` + +Returns `true` if ANY predicate matches. + +**Example:** + +```typescript +import { or, hasFormat } from '@quickflo/quickforms-vue' + +// Email OR URL format +const isEmailOrUrl = or( + hasFormat('email'), + hasFormat('url') +) + +console.log(isEmailOrUrl({ type: 'string', format: 'email' })) // true +console.log(isEmailOrUrl({ type: 'string', format: 'url' })) // true +console.log(isEmailOrUrl({ type: 'string' })) // false +``` + +### not + +```typescript +function not( + predicate: (schema: JSONSchema) => boolean +): (schema: JSONSchema) => boolean +``` + +Inverts a predicate. + +**Example:** + +```typescript +import { not, hasFormat } from '@quickflo/quickforms-vue' + +// String without a format +const isPlainString = not(hasFormat()) + +console.log(isPlainString({ type: 'string' })) // true +console.log(isPlainString({ type: 'string', format: 'email' })) // false +``` + +--- + +## Complete Examples + +### Example 1: Custom Phone Input + +```typescript +import { rankWith, and, isStringType, hasFormat } from '@quickflo/quickforms-vue' +import PhoneInput from './PhoneInput.vue' + +// Priority 10 for strings with phone format +const isPhoneField = rankWith(10, and( + isStringType, + hasFormat('phone') +)) + +registry.register('phone', PhoneInput, isPhoneField) + +// Use in schema +const schema = { + phone: { + type: 'string', + format: 'phone', // Triggers PhoneInput + title: 'Phone Number' + } +} +``` + +### Example 2: Custom Widget Selector + +```typescript +import { rankWith, hasExtension } from '@quickflo/quickforms-vue' +import ColorPicker from './ColorPicker.vue' +import SliderInput from './SliderInput.vue' + +// High priority for x-widget +const isColorWidget = rankWith(15, hasExtension('x-widget', 'color')) +const isSliderWidget = rankWith(15, hasExtension('x-widget', 'slider')) + +registry.register('color-picker', ColorPicker, isColorWidget) +registry.register('slider', SliderInput, isSliderWidget) + +// Use in schema +const schema = { + color: { + type: 'string', + 'x-widget': 'color', // Triggers ColorPicker + title: 'Theme Color' + }, + volume: { + type: 'number', + 'x-widget': 'slider', // Triggers SliderInput + title: 'Volume' + } +} +``` + +### Example 3: Override Built-in Component + +```typescript +import { rankWith, isStringType } from '@quickflo/quickforms-vue' +import CustomStringField from './CustomStringField.vue' + +// Built-in StringField has priority 2 +// Use priority 20 to override +const isCustomString = rankWith(20, isStringType) + +registry.register('custom-string', CustomStringField, isCustomString) + +// Now ALL string fields use CustomStringField +``` + +### Example 4: Conditional Component + +```typescript +import { rankWith, and, isNumberType } from '@quickflo/quickforms-vue' +import RangeSlider from './RangeSlider.vue' + +// Use slider for numbers with min/max +const isRangeNumber = rankWith(12, (schema) => + isNumberType(schema) && + schema.minimum !== undefined && + schema.maximum !== undefined +) + +registry.register('range-slider', RangeSlider, isRangeNumber) + +// Triggers RangeSlider +const schema = { + age: { + type: 'number', + minimum: 0, + maximum: 120 + } +} +``` + +--- + +## Built-in Component Priorities + +Reference for built-in component priorities: + +| Component | Tester | Priority | +|-----------|--------|----------| +| HiddenField | `x-hidden: true` | 100 | +| OneOfField | `hasOneOf` | 10 | +| AllOfField | `hasAllOf` | 10 | +| ArrayField | `isArrayType` | 5 | +| ObjectField | `isObjectType` | 5 | +| EnumField | `isEnumType` | 4 | +| DateField | `isDateFormat \| isTimeFormat \| isDateTimeFormat` | 3 | +| BooleanField | `isBooleanType` | 2 | +| NumberField | `isNumberType \| isIntegerType` | 2 | +| StringField | `isStringType` | 2 | + +**Strategy:** +- Use priority 1-5 for generic fallbacks +- Use priority 10-15 for specific overrides +- Use priority 20+ to override built-ins completely +- Use priority 100+ for critical components (hidden fields) + +--- + +## Best Practices + +1. **Start with high priority** - Use 10+ to ensure your component is selected +2. **Be specific** - Combine multiple predicates for precise targeting +3. **Test thoroughly** - Verify your tester matches only intended schemas +4. **Document behavior** - Comment why you're using specific priorities +5. **Avoid conflicts** - Two components at same priority = undefined behavior +6. **Use extensions** - Custom `x-*` properties are cleaner than complex predicates + +--- + +## Debugging + +Check which component will render: + +```typescript +const schema = { type: 'string', format: 'email' } +const component = registry.getComponent(schema) +console.log(component?.__name) // Component name +``` + +List all registered components: + +```typescript +// Access internal registry (not officially exposed) +console.log(registry._components) +``` + +--- + +## Next Steps + +- [Custom Components](/guide/custom-components) - Build and register custom components +- [Schema Extensions](/guide/schema-extensions) - Use `x-*` properties with testers +- [Components API](/guide/components) - All built-in components diff --git a/docs/guide/theming.md b/docs/guide/theming.md new file mode 100644 index 0000000..a968a5d --- /dev/null +++ b/docs/guide/theming.md @@ -0,0 +1,84 @@ +# Theming + +QuickForms styling approach depends on which package you're using. + +## Quasar + +If you're using the Quasar package, components automatically inherit your Quasar app's theme. + +**Options:** +- **Quasar SASS Variables** - Customize `quasar.variables.sass` for app-wide theming +- **Component Defaults** - Use `componentDefaults.global` for consistent QuickForms styling +- **Dark Mode** - Automatic support via Quasar's Dark plugin + +**Example:** + +```typescript +import { createQuasarRegistry } from '@quickflo/quickforms-quasar' + +const options = { + registry: createQuasarRegistry(), + componentDefaults: { + global: { + outlined: true, + dense: true, + color: 'primary' + } + } +} +``` + +See [Quasar Package - Theming](/guide/quasar#theming) for complete documentation. + +## Plain Vue + +If you're using the plain Vue package, QuickForms uses **CSS custom properties** (variables) for complete styling control. + +**60+ customizable variables** for colors, spacing, borders, typography, and more. + +**Example:** + +```css +:root { + /* Brand Colors */ + --quickform-color-primary: #8b5cf6; + --quickform-color-error: #ef4444; + + /* Spacing & Radius */ + --quickform-radius-md: 0.75rem; + --quickform-spacing-md: 1rem; + + /* Typography */ + --quickform-font-family: 'Inter', sans-serif; +} + +/* Dark Mode */ +.dark-theme { + --quickform-color-bg: #1f2937; + --quickform-color-text: #f3f4f6; + --quickform-color-border: #374151; +} +``` + +**Scoped Styling:** + +```vue + + + +``` + +See the main README's [STYLING_GUIDE.md](https://github.com/quickflo/quickforms/blob/main/STYLING_GUIDE.md) for the complete list of CSS variables. + +## Next Steps + +- [Quasar Theming](/guide/quasar#theming) - Quasar-specific options +- [Custom Components](/guide/custom-components) - Register custom styled components diff --git a/docs/guide/validation.md b/docs/guide/validation.md new file mode 100644 index 0000000..b08be1b --- /dev/null +++ b/docs/guide/validation.md @@ -0,0 +1,106 @@ +# Validation + +Learn about QuickForms' flexible validation system. + +## Overview + +QuickForms provides three levels of validation: + +1. **JSON Schema validation** - Built-in validation based on schema keywords +2. **Custom validators** - Add your own sync/async validation logic +3. **Validation modes** - Control when and how errors are displayed + +## Validation Modes + +Control validation behavior with the `validationMode` option: + +```vue + +``` + +- **`ValidateAndShow`** (default) - Validates as you type and displays errors +- **`ValidateAndHide`** - Validates but hides errors from user +- **`NoValidation`** - Completely disables validation + +## JSON Schema Validation + +QuickForms automatically validates based on JSON Schema keywords: + +```typescript +{ + email: { + type: 'string', + format: 'email', // Email format validation + minLength: 5, // Minimum length + maxLength: 100 // Maximum length + }, + age: { + type: 'number', + minimum: 18, // Minimum value + maximum: 120 // Maximum value + } +} +``` + +## Custom Error Messages + +Override default validation messages in two ways: + +### In Schema +```typescript +{ + password: { + type: 'string', + minLength: 8, + 'x-error-messages': { + required: 'Password is required', + minLength: 'Password must be at least 8 characters' + } + } +} +``` + +### In Form Options +```vue + +``` + +## Custom Validators + +See [Custom Validators Guide](/guide/custom-validators) for detailed information on adding custom validation logic. + +## Validation Events + +React to validation state changes: + +```vue + + + +``` + +## Next Steps + +- [Custom Validators](/guide/custom-validators) - Add sync/async validation +- [Schema Extensions](/guide/schema-extensions) - Learn about `x-error-messages` +- [Form Options API](/guide/form-options) - Complete validation options reference diff --git a/docs/guide/vue.md b/docs/guide/vue.md new file mode 100644 index 0000000..2b834f1 --- /dev/null +++ b/docs/guide/vue.md @@ -0,0 +1,86 @@ +# @quickflo/quickforms-vue + +Vue 3 bindings for QuickForms. + +## Installation + +::: code-group + +```sh [pnpm] +pnpm add @quickflo/quickforms @quickflo/quickforms-vue +``` + +```sh [npm] +npm install @quickflo/quickforms @quickflo/quickforms-vue +``` + +```sh [yarn] +yarn add @quickflo/quickforms @quickflo/quickforms-vue +``` + +::: + +## Components + +### DynamicForm + +Main form component that generates fields from JSON Schema. + +```vue + +``` + +**Props:** +- `schema` - JSON Schema definition +- `modelValue` - Form data (v-model) +- `options` - Form configuration + +**Events:** +- `@update:modelValue` - Form data changed +- `@validation` - Validation state changed + +See [Form Options API](/guide/form-options) for complete options reference. + +### Field Components + +Pre-built components for all field types: +- `StringField` - Text inputs +- `NumberField` - Number inputs +- `BooleanField` - Checkboxes +- `EnumField` - Select dropdowns +- `DateField` - Date/time pickers +- `ObjectField` - Nested objects +- `ArrayField` - Dynamic arrays +- `OneOfField` - Conditional schemas + +## Composables + +### useFormField + +Hook for field state management. + +```typescript +const { value, errorMessage, label, hint } = useFormField(path, schema) +``` + +### useFormContext + +Access form-level context. + +```typescript +const context = useFormContext() +// { readonly, disabled, schema, rootPath, context } +``` + +## Next Steps + +- [Getting Started](/guide/getting-started) - Build your first form +- [API Reference](/guide/components) - Complete component documentation +- [Examples](/guide/examples/basic-form) - See QuickForms in action diff --git a/docs/guide/what-is-quickforms.md b/docs/guide/what-is-quickforms.md new file mode 100644 index 0000000..4bc2ab6 --- /dev/null +++ b/docs/guide/what-is-quickforms.md @@ -0,0 +1,101 @@ +# What is QuickForms? + +QuickForms is a **Vue 3 JSON Schema form generator** designed with **sensible defaults and reasonable escape hatches**. + +## The Problem + +JSON Schema form libraries are powerful but often rigid: + +- **Hard to customize** - Changing simple things like placeholders requires rebuilding components +- **Design system lock-in** - Tightly coupled to Material-UI, Bootstrap, or custom frameworks +- **Limited validation** - JSON Schema alone can't handle cross-field or async validation +- **Poor DX** - Complex APIs, required UI schemas, or unclear customization paths + +## The QuickForms Approach + +QuickForms provides **escape hatches at common pain points**: + +- ✅ Don't like the default placeholder? Override it globally or per-field +- ✅ Need custom validation? Add sync/async validators alongside JSON Schema rules +- ✅ Enum values too technical? Map them to friendly labels with `x-enum-labels` +- ✅ Want dynamic hints? Use `hintRenderer` for full control +- ✅ Need custom components? Register them with the tester system + +**The philosophy**: Sensible defaults that work out of the box, with clear customization paths when you need them. + +## Key Features + +### 🚀 Framework-Agnostic Core +The core logic is framework-independent, making it easy to build bindings for React, Angular, or other frameworks in the future. + +### 📝 Full JSON Schema Support +- All primitive types (string, number, boolean) +- Complex types (objects, arrays, oneOf, anyOf, allOf) +- Validation keywords (minLength, pattern, minimum, etc.) +- Standard formats (email, url, date, time, date-time) + +### ✅ Flexible Validation +- **Three validation modes**: show errors, hide errors, or no validation +- **Custom validators**: Sync and async validation with automatic debouncing +- **Cross-field validation**: Validators have access to all form values +- **Custom error messages**: Override messages in schema or form options + +### 🎨 Themeable via CSS Variables +60+ CSS variables give you complete control over styling without rebuilding components. No design system lock-in. + +### 🔐 Built-in RBAC +Field-level visibility and editability control based on user roles. + +### 🌍 Internationalization Ready +Customize all UI labels and messages globally or per-form. + +### 🧩 Extensible Component System +Register custom components using a powerful tester system borrowed from jsonforms. + +## Architecture + +QuickForms is structured as a monorepo: + +``` +packages/ + core/ # Framework-agnostic logic + vue/ # Vue 3 bindings + quasar/ # Quasar component preset +``` + +### Core Package (`@quickflo/quickforms`) +Framework-agnostic TypeScript package containing: +- JSON Schema validation via Ajv +- Component registry with tester priority system +- Schema utilities (default values, path resolution) +- Type definitions + +### Vue Package (`@quickflo/quickforms-vue`) +Vue 3 Composition API bindings with VeeValidate integration: +- `DynamicForm` component +- Field components for all types +- Composables (`useFormField`, `useFormContext`) +- Custom component registration + +### Quasar Package (`@quickflo/quickforms-quasar`) +Pre-configured Quasar component renderers for zero-config usage. + +## When to Use QuickForms + +**Good fit:** +- Building admin panels, dashboards, or workflow engines +- Generating forms from API schemas (OpenAPI/JSON Schema) +- Need rapid form development with validation +- Want to iterate on form design without rebuilding components +- Working with dynamic schemas that change at runtime + +**Not a good fit:** +- Marketing landing pages with custom-designed forms +- Very simple forms (1-3 fields) where hand-coding is faster +- Need pixel-perfect control over every aspect of form layout + +## Next Steps + +- [Getting Started](/guide/getting-started) - Install and create your first form +- [Schema Basics](/guide/schema-basics) - Learn JSON Schema fundamentals +- [Examples](/guide/examples/basic-form) - See QuickForms in action diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..3cd2d36 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,170 @@ +--- +layout: home + +hero: + name: QuickForms + text: JSON Schema Form Generator + tagline: Vue 3 forms with sensible defaults and reasonable escape hatches + image: + src: /assets/banner.readme.1280x320.png + alt: QuickForms + actions: + - theme: brand + text: Get Started + link: /guide/getting-started + - theme: alt + text: View on GitHub + link: https://github.com/quickflo/quickforms + +features: + - icon: ⚡ + title: Fast & Lightweight + details: Framework-agnostic core with Vue 3 Composition API bindings. ~56KB gzipped. + + - icon: 🎨 + title: Themeable + details: 60+ CSS variables for complete styling control. No design system lock-in. + + - icon: 🔧 + title: Reasonable Escape Hatches + details: Override placeholders, add custom validators, map enum labels—without rebuilding components. + + - icon: 📝 + title: JSON Schema Powered + details: Full JSON Schema Draft 7+ support including oneOf, anyOf, nested objects, and arrays. + + - icon: ✅ + title: Flexible Validation + details: Three validation modes, custom sync/async validators, and cross-field validation. + + - icon: 🔐 + title: Role-Based Access + details: Built-in RBAC with field-level visibility and editability control. + + - icon: 🌍 + title: i18n Ready + details: Customize all labels and messages globally or per-form for internationalization. + + - icon: 🧩 + title: Extensible + details: Custom component registry with tester system for complete control over field rendering. +--- + +
+

See It In Action

+ +
+ QuickForms with Quasar +
+ +
+ QuickForms with Plain Vue +
+
+ +## Quick Example + +### Quasar + +If you're using Quasar, you get beautiful pre-styled components out of the box: + +```vue + + + +``` + +See [Quasar Package Docs](/guide/quasar) for all Quasar-specific options. + +### Plain Vue + +Use the plain Vue package with your own styling: + +```vue + + + +``` + +See [Vue Package Docs](/guide/vue) for plain Vue options. + +## Installation + +::: code-group + +```sh [pnpm] +pnpm add @quickflo/quickforms @quickflo/quickforms-vue @quickflo/quickforms-quasar +``` + +```sh [npm] +npm install @quickflo/quickforms @quickflo/quickforms-vue @quickflo/quickforms-quasar +``` + +```sh [yarn] +yarn add @quickflo/quickforms @quickflo/quickforms-vue @quickflo/quickforms-quasar +``` + +::: + +## Why QuickForms? + +JSON Schema form libraries are powerful but often rigid. QuickForms provides **escape hatches at common pain points**: + +- ✅ Don't like the default placeholder? Override it globally or per-field +- ✅ Need custom validation? Add sync/async validators alongside JSON Schema rules +- ✅ Enum values too technical? Map them to friendly labels with `x-enum-labels` +- ✅ Want dynamic hints? Use `hintRenderer` for full control + +**Sensible defaults, clear customization paths. No rebuilding components.** diff --git a/package.json b/package.json index 6105f04..943e27d 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,10 @@ "publish:core": "cd packages/core && pnpm publish --access public --no-git-checks", "publish:vue": "cd packages/vue && pnpm publish --access public --no-git-checks", "publish:quasar": "cd packages/quasar && pnpm publish --access public --no-git-checks", - "publish:all": "pnpm run prepublish && pnpm run publish:core && pnpm run publish:vue && pnpm run publish:quasar" + "publish:all": "pnpm run prepublish && pnpm run publish:core && pnpm run publish:vue && pnpm run publish:quasar", + "docs:dev": "vitepress dev docs", + "docs:build": "vitepress build docs", + "docs:preview": "vitepress preview docs" }, "keywords": [ "jsonschema", @@ -29,7 +32,9 @@ "devDependencies": { "@types/node": "^20.0.0", "typescript": "^5.3.0", - "vitest": "^1.0.0" + "vitepress": "^1.6.4", + "vitest": "^1.0.0", + "vue": "^3.5.24" }, "dependencies": { "@quickflo/quickforms": "^0.3.0", diff --git a/packages/core/package.json b/packages/core/package.json index 4fcc434..499214d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@quickflo/quickforms", - "version": "1.0.0", + "version": "1.1.0", "description": "Framework-agnostic core for QuickForms - JSON Schema form generator", "type": "module", "main": "./dist/index.js", diff --git a/packages/quasar/README.md b/packages/quasar/README.md index c8483cd..fcd1edd 100644 --- a/packages/quasar/README.md +++ b/packages/quasar/README.md @@ -174,6 +174,196 @@ const schema = { }; ``` +### QuickForms Convenience Features + +In addition to native Quasar props, QuickForms provides convenience features via `x-quickforms-quasar`. These are NOT native Quasar props - they're shortcuts we provide that get rendered into the appropriate Quasar component features (like icons into slots). + +**Why two namespaces?** +- `x-quasar-props` - Native Quasar component props (passed directly via `v-bind`) +- `x-quickforms-quasar` - QuickForms convenience features (interpreted and rendered by our components) + +This separation keeps the API clean and makes it clear what's native Quasar vs our convenience layer. + +#### Icon Support + +Add icons to input fields easily: + +```javascript +const schema = { + type: 'object', + properties: { + username: { + type: 'string', + title: 'Username', + 'x-quickforms-quasar': { + prependIcon: 'person', // Icon on the left + iconColor: 'primary' + } + }, + email: { + type: 'string', + format: 'email', + 'x-quickforms-quasar': { + prependIcon: 'mail' + } + }, + search: { + type: 'string', + 'x-quickforms-quasar': { + appendIcon: 'search', // Icon on the right + iconSize: 'md' + } + }, + password: { + type: 'string', + format: 'password', + // Password fields automatically get show/hide toggle + // You can still add a prepend icon: + 'x-quickforms-quasar': { + prependIcon: 'lock' + } + } + } +}; +``` + +**Available icon properties:** +- `prependIcon` - Icon name for left side of input +- `appendIcon` - Icon name for right side of input (not available for password fields or selects) +- `iconColor` - Quasar color for icons (default: `'grey-7'`) +- `iconSize` - Icon size: `'xs'`, `'sm'`, `'md'`, `'lg'`, `'xl'` (default: `'sm'`) + +**Icon names:** Use Material Icons names (Quasar's default): `person`, `mail`, `search`, `lock`, `visibility`, `phone`, etc. See [Material Icons](https://fonts.google.com/icons) for the full list. + +**Note:** Password fields automatically include a show/hide toggle icon in the append slot, so `appendIcon` is ignored for password fields. Select fields use the append slot for the dropdown indicator. + +#### Global Icon Defaults + +Set icon styling globally: + +```typescript +const formOptions: QuasarFormOptions = { + registry: createQuasarRegistry(), + componentDefaults: { + input: { + outlined: true + } + }, + quickformsDefaults: { + input: { + iconColor: 'primary', + iconSize: 'sm' + } + } +}; +``` + +#### Combining Native Props and Convenience Features + +You can use both namespaces together: + +```javascript +{ + type: 'string', + format: 'email', + // Native Quasar props + 'x-quasar-props': { + outlined: true, + dense: true, + clearable: true + }, + // QuickForms convenience features + 'x-quickforms-quasar': { + prependIcon: 'mail', + iconColor: 'primary' + } +} +``` + +#### Array Field Customization + +Customize array field buttons and layout with **full QBtn props support**: + +```javascript +const schema = { + type: 'object', + properties: { + tags: { + type: 'array', + items: { type: 'string' }, + title: 'Tags', + 'x-quickforms-quasar': { + addButtonPosition: 'top-right', // 'top-left', 'top-right', 'bottom-left', 'bottom-right' + addButton: { + // Native QBtn props - supports ALL Quasar button properties! + label: 'Add Tag', + icon: 'add_circle', + color: 'secondary', + size: 'md', + // Can use: push, fab, unelevated, glossy, etc. + }, + removeButton: { + icon: 'delete', + color: 'negative', + } + } + }, + team: { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + role: { type: 'string' } + } + }, + 'x-quickforms-quasar': { + addButtonPosition: 'bottom-right', + addButton: { + label: 'Add Team Member', + icon: 'person_add', + color: 'positive', + } + } + } + } +}; +``` + +**Available array customization properties:** + +- `addButtonPosition` - Position: `'top-left'`, `'top-right'`, `'bottom-left'`, `'bottom-right'` (default: `'bottom-left'`) +- `addButton` - **Native QBtn props** (supports ALL Quasar button properties) + - Default: `{ outline: true, color: 'primary', icon: 'add', label: 'Add item' }` + - Examples: `label`, `icon`, `color`, `size`, `class`, `style`, `push`, `fab`, `glossy`, `unelevated`, etc. +- `removeButton` - **Native QBtn props** (supports ALL Quasar button properties) + - Default: `{ flat: true, round: true, dense: true, size: 'sm', icon: 'close', color: 'negative' }` + +**Why this approach?** Instead of us defining individual properties like `addButtonLabel`, `addButtonIcon`, etc., we pass native Quasar `QBtn` props directly. This gives you access to **ALL** QBtn features without limitation! + +**Set global array defaults:** + +```typescript +const formOptions: QuasarFormOptions = { + registry: createQuasarRegistry(), + quickformsDefaults: { + array: { + // Apply to ALL arrays unless overridden per-field + addButtonPosition: 'top-right', + addButton: { + color: 'secondary', + size: 'lg', + glossy: true, + }, + removeButton: { + icon: 'delete', + color: 'warning', + }, + }, + }, +}; +``` + ### Custom Component Registration ```vue diff --git a/packages/quasar/dev/App.vue b/packages/quasar/dev/App.vue index ca3cfc3..f8d526f 100644 --- a/packages/quasar/dev/App.vue +++ b/packages/quasar/dev/App.vue @@ -2,12 +2,17 @@ import { ref } from "vue"; import { DynamicForm, JSONSchema } from "@quickflo/quickforms-vue"; import { createQuasarRegistry, QuasarFormOptions } from "../src/index"; +import Showcase from "./Showcase.vue"; + +const showShowcase = ref(true); const registry = createQuasarRegistry(); // Global Quasar defaults applied to all components const formOptions: QuasarFormOptions = { registry, + validateOnMount: false, + componentDefaults: { global: { outlined: true, // Apply outlined style to ALL Quasar components @@ -15,16 +20,35 @@ const formOptions: QuasarFormOptions = { }, input: { // Add clearable button to all inputs - clearable: true, + clearable: false, }, select: { - // Use chips for enum fields - useChips: true, // Uncomment to enable + clearable: true, }, checkbox: { color: "green", }, }, + // QuickForms convenience features + quickformsDefaults: { + input: { + iconColor: "grey-7", + iconSize: "sm", + }, + + array: { + // Global defaults for all arrays (can be overridden per-field) + addButtonPosition: "bottom-left", + addButton: { + // Full QBtn props supported! + color: "primary", + outline: true, + }, + removeButton: { + color: "negative", + }, + }, + }, }; const formData = ref({}); @@ -132,13 +156,122 @@ const schema: JSONSchema = { tags: { type: "array", title: "Tags", - description: "Add relevant tags", + description: "Simple array with custom add button", items: { type: "string", title: "Tag", + minLength: 1, }, - "x-quasar-props": { - dense: false, + "x-quickforms-quasar": { + addButtonPosition: "top-right", + addButton: { + label: "Add Tag", + icon: "add_circle", + color: "secondary", + }, + removeButton: { + icon: "delete", + }, + }, + }, + + // === ARRAY OF OBJECTS === + teamMembers: { + type: "array", + title: "Team Members", + description: "Array of objects with custom labels and positioning", + items: { + type: "object", + title: "Team Member", + properties: { + name: { + type: "string", + title: "Name", + minLength: 2, + }, + role: { + type: "string", + title: "Role", + enum: ["Developer", "Designer", "Manager", "QA"], + }, + email: { + type: "string", + format: "email", + title: "Email", + }, + startDate: { + type: "string", + format: "date", + title: "Start Date", + }, + }, + required: ["name", "role"], + }, + minItems: 0, + maxItems: 5, + "x-item-label": "{{name}} - {{role}}", + "x-quickforms-quasar": { + addButtonPosition: "bottom-right", + addButton: { + label: "Add Team Member", + icon: "person_add", + color: "positive", + + // Can use ANY QBtn prop: size, fab, push, unelevated, etc. + }, + removeButton: { + icon: "person_remove", + color: "negative", + }, + }, + }, + + // === ARRAY WITH DIFFERENT BUTTON POSITIONS === + todos: { + type: "array", + title: "Todo List (Add button: bottom-left)", + items: { + type: "object", + properties: { + task: { + type: "string", + title: "Task", + }, + priority: { + type: "string", + enum: ["low", "medium", "high"], + title: "Priority", + }, + completed: { + type: "boolean", + title: "Completed", + }, + }, + }, + "x-item-label": "{{task}}", + "x-quickforms-quasar": { + addButtonPosition: "bottom-left", + addButton: { + label: "Add Todo", + }, + }, + }, + + // === ARRAY WITH TOP-LEFT BUTTON === + notes: { + type: "array", + title: "Notes (Add button: top-left)", + description: "Demonstrates top-left button positioning", + items: { + type: "string", + title: "Note", + }, + "x-quickforms-quasar": { + addButtonPosition: "top-left", + addButton: { + label: "New Note", + icon: "note_add", + }, }, }, // === CONST FIELD (HIDDEN) === @@ -180,16 +313,44 @@ const schema: JSONSchema = { }, }, - // === PASSWORD FIELD === + // === PASSWORD FIELD WITH ICON === password: { type: "string", format: "password", title: "Password", - description: "Password field with show/hide toggle", + description: "Password field with show/hide toggle and prepend icon", minLength: 8, "x-quasar-props": { dense: false, }, + "x-quickforms-quasar": { + prependIcon: "lock", + iconColor: "grey-7", + }, + }, + + // === TEXT FIELD WITH ICONS === + username: { + type: "string", + title: "Username", + description: "Input field with prepend icon", + minLength: 3, + "x-quickforms-quasar": { + prependIcon: "person", + iconColor: "primary", + }, + }, + + // === SEARCH FIELD WITH APPEND ICON === + search: { + type: "string", + title: "Search", + description: "Input with append icon", + "x-quickforms-quasar": { + appendIcon: "search", + iconColor: "grey-6", + iconSize: "md", + }, }, // === URL FIELD === @@ -346,10 +507,12 @@ const handleSubmit = () => {
- Features shown: Const fields (hidden), - autocomplete, password, URL, textarea, numbers with - prefix/suffix, nested objects, arrays, oneOf (conditional), - date/time pickers, and more! + Features shown: Icon customization + (prepend/append), array button positioning (top/bottom, + left/right), arrays of objects, custom item labels, password + show/hide toggle, const fields (hidden), autocomplete, + nested objects, oneOf (conditional), date/time pickers, and + more!
diff --git a/packages/quasar/dev/Showcase.vue b/packages/quasar/dev/Showcase.vue new file mode 100644 index 0000000..783a0be --- /dev/null +++ b/packages/quasar/dev/Showcase.vue @@ -0,0 +1,210 @@ + + + + + diff --git a/packages/quasar/package.json b/packages/quasar/package.json index 8ffba4d..44182ea 100644 --- a/packages/quasar/package.json +++ b/packages/quasar/package.json @@ -1,6 +1,6 @@ { "name": "@quickflo/quickforms-quasar", - "version": "1.0.0", + "version": "1.1.0", "description": "Quasar UI components for QuickForms - JSON Schema form generator", "type": "module", "main": "./dist/index.js", diff --git a/packages/quasar/src/components/QuasarArrayField.vue b/packages/quasar/src/components/QuasarArrayField.vue index 1189583..7377ccc 100644 --- a/packages/quasar/src/components/QuasarArrayField.vue +++ b/packages/quasar/src/components/QuasarArrayField.vue @@ -1,11 +1,12 @@