Skip to content

Custom Components Documentation

Guide to using custom form components and UI elements in Meister Bill


Table of Contents


Form Components

All form components are built on top of Formwerk, a lightweight form validation library with full TypeScript support.

FCheckbox

Location: apps/web/components/F/Checkbox.vue

A checkbox component with Formwerk integration, error handling, and support for custom label content.

Basic Usage

<template>
  <form>
    <!-- Simple checkbox with label prop -->
    <FCheckbox
      label="Primary Address"
      name="is_primary"
    />

    <!-- Checkbox with validation -->
    <FCheckbox
      label="Accept Newsletter"
      name="newsletter_accepted"
    />
  </form>
</template>

Advanced Usage with Slot

The FCheckbox component supports a default slot for custom label content, such as labels with inline links:

<template>
  <form>
    <!-- Checkbox with link in label -->
    <FCheckbox name="terms_accepted">
      {{ $t('label.i_accept_the') }}
      <NuxtLink
        :to="$localePath('info-terms-of-use')"
        target="_blank"
        class="link link-primary"
      >
        {{ $t('label.terms_and_conditions') }}
      </NuxtLink>
    </FCheckbox>

    <!-- Checkbox with formatted content -->
    <FCheckbox name="subscribe">
      Subscribe to our newsletter and get
      <strong>10% off</strong> your first invoice!
    </FCheckbox>
  </form>
</template>

Props

Prop Type Required Description
name string Yes Form field name
label string No Simple text label (not used when slot is provided)
Other props from Formwerk's CheckboxProps See Formwerk Checkbox docs

Slots

Slot Description
default Custom label content. If provided, the label prop is ignored.

Features

  • ✅ Formwerk integration for validation
  • ✅ Error message display
  • ✅ Dark mode support
  • ✅ DaisyUI styling
  • ✅ Custom label content via slot
  • ✅ Backward compatible (label prop still works)

Implementation Details

The component checks for the presence of a default slot: - If slot exists: Renders slot content - If no slot: Falls back to label prop

This ensures backward compatibility with existing code while enabling new use cases.


FTextField

Location: apps/web/components/F/TextField.vue

A text input component with Formwerk integration.

Basic Usage

<FTextField
  label="Email"
  name="email"
  type="email"
  :placeholder="$t('label.email')"
/>

<FTextField
  label="Password"
  name="password"
  type="password"
  :placeholder="$t('label.password')"
/>

FErrorMessage

Location: apps/web/components/F/ErrorMessage.vue

Displays validation error messages in a consistent style.

Usage

Error messages are automatically displayed by form components when validation fails. The component is used internally by FCheckbox, FTextField, and other form components.


Component Patterns

Form Integration with Formwerk

All custom form components use Formwerk's composables:

<script setup lang="ts">
import { type CheckboxProps, useCheckbox } from "@formwerk/core";

const props = defineProps<CheckboxProps>();
const { labelProps, inputProps, errorMessage, isTouched } = useCheckbox(props);
</script>

Schema Validation

Forms use Zod schemas for validation:

import { SignUpSchema } from "@meisterbill/schemas";

const { handleSubmit, values } = useForm<SignUp>({
  schema: SignUpSchema,
  initialValues: props.initialValues || {},
});

Example schema with checkbox validation:

export const SignUpSchema = z.object({
  email: z.string().email(),
  password: PasswordSchema,
  terms_accepted: z.literal(true, { error: "accept_terms" }),
});

Best Practices

1. Use Slots for Complex Labels

When labels need inline links, formatting, or dynamic content:

<!-- ✅ Good: Use slot -->
<FCheckbox name="terms">
  I accept the <NuxtLink to="/terms">Terms</NuxtLink>
</FCheckbox>

<!-- ❌ Avoid: Complex HTML in label prop -->
<FCheckbox :label="`I accept the <a href='/terms'>Terms</a>`" name="terms" />

2. Leverage i18n for Labels

Always use i18n for user-facing text:

<FCheckbox name="newsletter">
  {{ $t('label.subscribe_to') }}
  <strong>{{ $t('label.newsletter') }}</strong>
</FCheckbox>

3. Validation Messages

Define validation errors in the schema with i18n keys:

terms_accepted: z.literal(true, { error: "accept_terms" })

Then add the translation:

{
  "message": {
    "accept_terms": "You must accept the terms and conditions"
  }
}

4. Consistent Styling

Use DaisyUI classes for consistent styling:

<NuxtLink to="/terms" class="link link-primary">
  Terms and Conditions
</NuxtLink>

5. Accessibility

  • Always provide meaningful labels or slot content
  • Use semantic HTML elements
  • Ensure links open in new tabs when appropriate with target="_blank"

Examples

Sign-Up Form with Terms Acceptance

<template>
  <form @submit="onSubmit">
    <FTextField
      label="Name"
      name="name"
      type="text"
      :placeholder="$t('label.name')"
    />

    <FTextField
      label="Email"
      name="email"
      type="email"
      :placeholder="$t('label.email')"
    />

    <FTextField
      label="Password"
      name="password"
      type="password"
      :placeholder="$t('label.password')"
    />

    <FCheckbox name="terms_accepted">
      {{ $t('label.i_accept_the') }}
      <NuxtLink
        :to="$localePath('info-terms-of-use')"
        target="_blank"
        class="link link-primary"
      >
        {{ $t('label.terms_and_conditions') }}
      </NuxtLink>
    </FCheckbox>

    <button type="submit" class="btn btn-primary">
      {{ $t('label.sign_up') }}
    </button>
  </form>
</template>

<script setup lang="ts">
import { type SignUp, SignUpSchema } from "@meisterbill/schemas";
import { useForm } from "@formwerk/core";

const { handleSubmit, values } = useForm<SignUp>({
  schema: SignUpSchema,
  initialValues: {},
});

const onSubmit = handleSubmit((data) => {
  // Handle form submission
  console.log(data.toObject());
});
</script>

Migration Guide

Before:

<fieldset class="fieldset">
  <label class="label">
    <input type="checkbox" name="terms" />
    I accept the <a href="/terms">Terms</a>
  </label>
</fieldset>

After:

<FCheckbox name="terms">
  I accept the <NuxtLink to="/terms">Terms</NuxtLink>
</FCheckbox>


Last updated: 2025-10-05