# Standard Form Template - ROA2WEB

**Version:** 1.0
**Date:** 2025-11-18
**Status:** Active

---

## Overview

This document provides the standard template and guidelines for creating forms in ROA2WEB. All forms should use the centralized `forms.css` pattern library to ensure consistency, accessibility, and maintainability.

---

## Quick Reference

### Available Classes

| Class | Purpose | Example |
|-------|---------|---------|
| `.form` | Form container | `<form class="form">` |
| `.form-group` | Field wrapper | `<div class="form-group">` |
| `.form-label` | Label styling | `<label class="form-label">` |
| `.form-label.required` | Required field label (adds *) | `<label class="form-label required">` |
| `.form-input` | Native input styling | `<input class="form-input">` |
| `.form-select` | Native select styling | `<select class="form-select">` |
| `.form-textarea` | Textarea styling | `<textarea class="form-textarea">` |
| `.form-error` | Error message | `<span class="form-error">` |
| `.form-help` | Help text | `<span class="form-help">` |
| `.form-row` | Horizontal field layout | `<div class="form-row">` |
| `.form-col` | Column in horizontal layout | `<div class="form-col">` |
| `.invalid` | Validation error state | `:class="{ 'invalid': errors.field }"` |
| `.valid` | Validation success state | `:class="{ 'valid': !errors.field }"` |

---

## Basic Form Structure

### Single Column Form

```vue
<template>
  <form @submit.prevent="handleSubmit" class="form">
    <!-- Single field -->
    <div class="form-group">
      <label for="username" class="form-label required">Utilizator</label>
      <input
        id="username"
        v-model="formData.username"
        type="text"
        class="form-input"
        :class="{ 'invalid': errors.username }"
        placeholder="Introduceți numele de utilizator"
      />
      <span v-if="errors.username" class="form-error">
        {{ errors.username }}
      </span>
      <span v-else class="form-help">
        Minimum 3 caractere
      </span>
    </div>

    <!-- Password field -->
    <div class="form-group">
      <label for="password" class="form-label required">Parolă</label>
      <input
        id="password"
        v-model="formData.password"
        type="password"
        class="form-input"
        :class="{ 'invalid': errors.password }"
        placeholder="Introduceți parola"
      />
      <span v-if="errors.password" class="form-error">
        {{ errors.password }}
      </span>
    </div>

    <!-- Form actions -->
    <div class="form-actions">
      <button type="button" class="btn btn-secondary" @click="handleCancel">
        Anulează
      </button>
      <button type="submit" class="btn btn-primary" :disabled="!isValid">
        Salvează
      </button>
    </div>
  </form>
</template>

<script setup>
import { ref, computed } from 'vue';

const formData = ref({
  username: '',
  password: ''
});

const errors = ref({
  username: '',
  password: ''
});

const isValid = computed(() => {
  return formData.value.username.length >= 3 &&
         formData.value.password.length >= 6 &&
         !errors.value.username &&
         !errors.value.password;
});

const handleSubmit = () => {
  // Submit logic
};

const handleCancel = () => {
  // Cancel logic
};
</script>
```

---

## Multi-Column Form (Horizontal Layout)

```vue
<template>
  <form @submit.prevent="handleSubmit" class="form">
    <!-- Horizontal row with 2 fields -->
    <div class="form-row">
      <div class="form-col">
        <div class="form-group">
          <label for="firstName" class="form-label required">Prenume</label>
          <input
            id="firstName"
            v-model="formData.firstName"
            type="text"
            class="form-input"
            :class="{ 'invalid': errors.firstName }"
          />
          <span v-if="errors.firstName" class="form-error">
            {{ errors.firstName }}
          </span>
        </div>
      </div>

      <div class="form-col">
        <div class="form-group">
          <label for="lastName" class="form-label required">Nume</label>
          <input
            id="lastName"
            v-model="formData.lastName"
            type="text"
            class="form-input"
            :class="{ 'invalid': errors.lastName }"
          />
          <span v-if="errors.lastName" class="form-error">
            {{ errors.lastName }}
          </span>
        </div>
      </div>
    </div>

    <!-- Horizontal row with 3 fields -->
    <div class="form-row">
      <div class="form-col">
        <div class="form-group">
          <label for="city" class="form-label">Oraș</label>
          <input id="city" v-model="formData.city" type="text" class="form-input" />
        </div>
      </div>

      <div class="form-col">
        <div class="form-group">
          <label for="county" class="form-label">Județ</label>
          <select id="county" v-model="formData.county" class="form-select">
            <option value="">Selectați...</option>
            <option value="AB">Alba</option>
            <option value="BV">Brașov</option>
          </select>
        </div>
      </div>

      <div class="form-col">
        <div class="form-group">
          <label for="zip" class="form-label">Cod Poștal</label>
          <input id="zip" v-model="formData.zip" type="text" class="form-input" />
        </div>
      </div>
    </div>

    <div class="form-actions">
      <button type="submit" class="btn btn-primary">Salvează</button>
    </div>
  </form>
</template>
```

---

## PrimeVue Components Integration

### InputText

```vue
<div class="form-group">
  <label for="email" class="form-label required">Email</label>
  <InputText
    id="email"
    v-model="formData.email"
    type="email"
    :class="{ 'invalid': errors.email }"
    class="w-full"
    placeholder="email@example.com"
  />
  <span v-if="errors.email" class="form-error">
    {{ errors.email }}
  </span>
</div>
```

### Password

```vue
<div class="form-group">
  <label for="password" class="form-label required">Parolă</label>
  <Password
    id="password"
    v-model="formData.password"
    :class="{ 'invalid': errors.password }"
    class="w-full"
    :feedback="false"
    toggle-mask
  />
  <span v-if="errors.password" class="form-error">
    {{ errors.password }}
  </span>
</div>
```

### Dropdown

```vue
<div class="form-group">
  <label for="company" class="form-label required">Companie</label>
  <Dropdown
    id="company"
    v-model="formData.companyId"
    :options="companies"
    option-label="name"
    option-value="id"
    :class="{ 'invalid': errors.companyId }"
    class="w-full"
    placeholder="Selectați compania"
  />
  <span v-if="errors.companyId" class="form-error">
    {{ errors.companyId }}
  </span>
</div>
```

### Calendar (Date Picker)

```vue
<div class="form-row">
  <div class="form-col">
    <div class="form-group">
      <label for="dateFrom" class="form-label">Data De</label>
      <Calendar
        id="dateFrom"
        v-model="formData.dateFrom"
        date-format="dd/mm/yy"
        class="w-full"
        placeholder="Selectați data"
      />
    </div>
  </div>

  <div class="form-col">
    <div class="form-group">
      <label for="dateTo" class="form-label">Data Până</label>
      <Calendar
        id="dateTo"
        v-model="formData.dateTo"
        date-format="dd/mm/yy"
        class="w-full"
        placeholder="Selectați data"
      />
    </div>
  </div>
</div>
```

### Textarea

```vue
<div class="form-group">
  <label for="description" class="form-label">Descriere</label>
  <textarea
    id="description"
    v-model="formData.description"
    class="form-textarea"
    rows="4"
    placeholder="Introduceți descrierea..."
  ></textarea>
  <span class="form-help">Maximum 500 caractere</span>
</div>
```

---

## Filter Forms

For filter/search forms (like in InvoicesView), use horizontal layout:

```vue
<template>
  <Card class="filters-card">
    <template #content>
      <div class="form">
        <div class="form-row">
          <div class="form-col">
            <div class="form-group">
              <label class="form-label">Tip Factură</label>
              <Dropdown
                v-model="filters.type"
                :options="invoiceTypes"
                option-label="label"
                option-value="value"
                placeholder="Tip factură"
                class="w-full"
                @change="handleFilterChange"
              />
            </div>
          </div>

          <div class="form-col">
            <div class="form-group">
              <label class="form-label">Data De</label>
              <Calendar
                v-model="filters.dateFrom"
                date-format="dd/mm/yy"
                placeholder="Selectați data"
                class="w-full"
                @date-select="handleFilterChange"
              />
            </div>
          </div>

          <div class="form-col">
            <div class="form-group">
              <label class="form-label">Data Până</label>
              <Calendar
                v-model="filters.dateTo"
                date-format="dd/mm/yy"
                placeholder="Selectați data"
                class="w-full"
                @date-select="handleFilterChange"
              />
            </div>
          </div>

          <div class="form-col search-col">
            <div class="form-group">
              <label class="form-label">Căutare</label>
              <InputText
                v-model="filters.searchTerm"
                placeholder="Căutați..."
                class="w-full"
                @input="handleSearchChange"
              />
            </div>
          </div>
        </div>

        <!-- Filter actions -->
        <div class="filters-actions">
          <Button
            icon="pi pi-filter-slash"
            label="Resetează Filtre"
            class="p-button-outlined p-button-secondary"
            @click="clearFilters"
          />
          <Button
            icon="pi pi-refresh"
            label="Actualizează"
            @click="refreshData"
          />
        </div>
      </div>
    </template>
  </Card>
</template>

<style scoped>
.filters-card {
  margin-bottom: 2rem;
}

.search-col {
  grid-column: span 2;
}

.filters-actions {
  display: flex;
  gap: 1rem;
  justify-content: flex-end;
  padding-top: 1rem;
  border-top: 1px solid var(--surface-border);
}

/* Mobile responsive handled by forms.css */
@media (max-width: 768px) {
  .search-col {
    grid-column: span 1;
  }

  .filters-actions {
    flex-direction: column;
  }
}
</style>
```

---

## Validation

### Client-Side Validation Pattern

```vue
<script setup>
import { ref, computed } from 'vue';

const formData = ref({
  email: '',
  password: '',
  confirmPassword: ''
});

const errors = ref({
  email: '',
  password: '',
  confirmPassword: ''
});

// Field-level validation
const validateField = (field) => {
  switch (field) {
    case 'email':
      const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
      errors.value.email = !emailRegex.test(formData.value.email)
        ? 'Email invalid'
        : '';
      break;

    case 'password':
      errors.value.password = formData.value.password.length < 6
        ? 'Parola trebuie să aibă minimum 6 caractere'
        : '';
      break;

    case 'confirmPassword':
      errors.value.confirmPassword = formData.value.password !== formData.value.confirmPassword
        ? 'Parolele nu corespund'
        : '';
      break;
  }
};

// Form-level validation
const isFormValid = computed(() => {
  return formData.value.email &&
         formData.value.password &&
         formData.value.confirmPassword &&
         !errors.value.email &&
         !errors.value.password &&
         !errors.value.confirmPassword;
});
</script>
```

---

## Accessibility Guidelines

### Required Standards

1. **Labels**: Every input must have an associated `<label>` with `for` attribute
   ```vue
   <label for="username" class="form-label required">Utilizator</label>
   <input id="username" ... />
   ```

2. **Required Fields**: Use `.form-label.required` class (adds red asterisk)
   ```vue
   <label class="form-label required">Email</label>
   ```

3. **Error Messages**: Use `.form-error` with descriptive text
   ```vue
   <span v-if="errors.email" class="form-error">
     Email-ul este obligatoriu
   </span>
   ```

4. **Help Text**: Use `.form-help` for hints
   ```vue
   <span class="form-help">Minimum 8 caractere</span>
   ```

5. **ARIA Attributes**: Add for screen readers
   ```vue
   <input
     :aria-invalid="!!errors.username"
     aria-describedby="username-error"
   />
   <span id="username-error" class="form-error">{{ errors.username }}</span>
   ```

6. **Keyboard Navigation**: Ensure all inputs are keyboard accessible (handled by forms.css)

7. **Touch Targets**: Minimum 44px height for mobile (handled by forms.css)

---

## Mobile Responsive Behavior

The `forms.css` file automatically handles mobile responsiveness:

- **Desktop (>768px)**: `.form-row` displays as grid with multiple columns
- **Mobile (≤768px)**: `.form-row` stacks vertically (flex-direction: column)

No additional CSS required in components!

---

## Common Patterns

### Login Form

See: `src/views/LoginView.vue` (lines 14-47)

### Filter Form

See: `src/views/InvoicesView.vue` (lines 36-99)

### Search Form

```vue
<div class="search-form">
  <div class="search-input">
    <input type="text" class="form-input" placeholder="Căutare..." />
    <i class="search-icon pi pi-search"></i>
  </div>
  <button type="submit" class="btn btn-primary">
    <i class="pi pi-search"></i> Caută
  </button>
</div>
```

---

## Anti-Patterns (DO NOT DO)

### ❌ Don't Create Custom Field Wrappers

```vue
<!-- BAD -->
<div class="custom-field">
  <label class="custom-label">Email</label>
  <input class="custom-input" />
</div>

<!-- GOOD -->
<div class="form-group">
  <label class="form-label">Email</label>
  <input class="form-input" />
</div>
```

### ❌ Don't Use `:deep()` for PrimeVue Overrides

```vue
<!-- BAD -->
<style scoped>
:deep(.p-inputtext) {
  border: 2px solid #ccc !important;
}
</style>

<!-- GOOD -->
<!-- All PrimeVue overrides go in global primevue-overrides.css (Phase 3) -->
```

### ❌ Don't Hardcode Colors

```vue
<!-- BAD -->
.form-label {
  color: #333333;
}

<!-- GOOD -->
.form-label {
  color: var(--text-color);
}
```

### ❌ Don't Duplicate Form CSS

```vue
<!-- BAD -->
<style scoped>
.field {
  margin-bottom: 1.5rem;
}
.field-label {
  display: block;
  font-weight: 600;
}
</style>

<!-- GOOD -->
<!-- Just use .form-group and .form-label from forms.css -->
```

---

## Testing Checklist

Before committing form changes:

- [ ] All labels have `for` attribute matching input `id`
- [ ] Required fields use `.form-label.required`
- [ ] Error messages use `.form-error`
- [ ] Validation states use `.invalid` or `.valid` classes
- [ ] Form is keyboard navigable (Tab through fields)
- [ ] Form is responsive on mobile (test at 375px width)
- [ ] No custom form CSS in component (use forms.css)
- [ ] Playwright visual regression tests pass
- [ ] No console errors

---

## Migration Guide

### Migrating Existing Forms

1. **Update Template**:
   - Replace custom field wrappers with `.form-group`
   - Replace custom labels with `.form-label`
   - Add `.w-full` to PrimeVue components
   - Change `p-invalid` to `invalid`
   - Change `<small class="p-error">` to `<span class="form-error">`

2. **Remove Custom CSS**:
   - Delete field wrapper styles
   - Delete label styles
   - Delete custom validation styles
   - Delete `:deep()` PrimeVue overrides

3. **Test**:
   - Run visual regression tests
   - Manual test on desktop/tablet/mobile
   - Verify keyboard navigation
   - Check validation states

---

## Support & Questions

- **Pattern Library**: `src/assets/css/components/forms.css`
- **Examples**: `src/views/LoginView.vue`, `src/views/InvoicesView.vue`
- **CSS Refactoring Plan**: `features/CSS_REFACTORING_PLAN.md`
- **Issues**: Document in PROGRESS_TRACKER.md

---

**Last Updated:** 2025-11-18
**Phase:** 1 - Forms Standardization
**Status:** Complete
