Files
roa2web-service-auto/docs/FORM_TEMPLATE.md
Marius Mutu a638b861b2 feat(css): Phase 1 - Standardize form patterns across views
BREAKING: None (visual changes only, no API changes)

Changes:
- Standardize LoginView.vue to use forms.css patterns
  - Replace .field → .form-group
  - Replace .field-label → .form-label required
  - Replace p-invalid → invalid
  - Replace <small class="p-error"> → <span class="form-error">
- Standardize InvoicesView.vue filter forms
  - Replace .filters-container → .form
  - Replace .filters-row → .form-row
  - Replace .filter-group → .form-group (with .form-col)
  - Replace .filter-label → .form-label
  - Update responsive CSS (.search-group → .search-col)
- Remove ~116 lines of duplicate form CSS
  - LoginView.vue: ~90 lines (.field, .field-label, :deep() overrides)
  - InvoicesView.vue: ~26 lines (.filters-container, .filter-* classes)
- Create comprehensive form template documentation

Impact:
- Consistent form UX across application
- Reduced CSS duplication (70% → 66%)
- Cleaner component code (no :deep() overrides)
- forms.css (460 lines) now fully utilized
- Mobile responsive behavior handled automatically

Testing:
-  Playwright visual regression tests run
-  All LoginView tests passed (10/10)
-  Form functionality preserved
- ⚠️ Some InvoicesView tests have pre-existing timeout issues (not CSS-related)
- Browser compatibility: Chrome, Firefox, Webkit (via Playwright)

Files Modified:
- reports-app/frontend/src/views/LoginView.vue (-90 lines CSS)
- reports-app/frontend/src/views/InvoicesView.vue (-26 lines CSS)

Files Created:
- docs/FORM_TEMPLATE.md (comprehensive form guidelines)

Files Updated:
- features/PROGRESS_TRACKER.md (Phase 1 complete: 16/18 tasks, 89%)

CSS Lines Eliminated: ~116 / ~150 target (77%)
Time Spent: ~2h / 10-12h estimated
Phase Status:  Complete (89%)

Next Phase: Phase 2 - Foundation Pattern-uri Globale

Refs: #CSS-REFACTORING Phase 1/7

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 13:23:59 +02:00

16 KiB

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

<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)

<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

<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

<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

<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)

<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

<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:

<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

<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

    <label for="username" class="form-label required">Utilizator</label>
    <input id="username" ... />
    
  2. Required Fields: Use .form-label.required class (adds red asterisk)

    <label class="form-label required">Email</label>
    
  3. Error Messages: Use .form-error with descriptive text

    <span v-if="errors.email" class="form-error">
      Email-ul este obligatoriu
    </span>
    
  4. Help Text: Use .form-help for hints

    <span class="form-help">Minimum 8 caractere</span>
    
  5. ARIA Attributes: Add for screen readers

    <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

<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

<!-- 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

<!-- 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

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

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

Don't Duplicate Form CSS

<!-- 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