13 KiB
Phase 2 Step 1: Typed Fields Implementation
Status: 5/12 fields scaffolded, 7 templates provided
Date: 2026-08-12 (Week 3 start)
Timeline: Week 3-4 (2 weeks)
Typed Fields Overview (Layer 2)
Purpose: Domain-aware input components with automatic validation, formatting, and user-friendly error messages
12 Total Typed Fields:
- TextField ✅ (text, email, password, url, tel)
- DateField ✅ (date picker with min/max)
- CurrencyField ✅ (amount with locale formatting)
- SelectField ✅ (dropdown with validation)
- StatusField ✅ (predefined statuses with colors)
- TimeField (time picker)
- PercentageField (0-100% with formatting)
- QuantityField (positive integer, no decimals)
- MultiSelectField (multiple selections)
- CheckboxField (boolean checkbox)
- SearchField (autocomplete with API lookup)
- PhoneField (phone number with formatting)
Completed: 5 Typed Fields ✅
1. TextField
<!-- Features -->
- Type support: text, email, password, url, tel
- Validation rules (required, email, url, pattern, minLength, maxLength)
- Character counter
- Help text + error messages
- WCAG 2.1 AA accessibility
Props: modelValue, label, type, placeholder, disabled, required, maxLength, validationRules, etc.
2. DateField
<!-- Features -->
- HTML5 date picker (native)
- Min/Max date validation
- ISO format (YYYY-MM-DD)
- Locale-aware display
- Range validation
Props: modelValue (ISO date), label, minDate, maxDate, disabled, required, etc.
3. CurrencyField
<!-- Features -->
- Locale formatting (₩ Korean Won, comma separators)
- Input validation (positive, decimals)
- Currency symbol display
- Min/Max amount validation
- Decimal precision (default: 0, customizable)
Props: modelValue, currencySymbol (₩), minValue, maxValue, decimals, etc.
4. SelectField
<!-- Features -->
- Dropdown with typed options
- Placeholder support
- Required validation
- WCAG 2.1 accessibility
- Search-ready (for future autocomplete)
Props: modelValue, options (Array<{value, label}>), required, etc.
5. StatusField
<!-- Features -->
- Predefined statuses: DRAFT, PENDING, APPROVED, ACTIVE, COMPLETED, CANCELLED, FAILED
- Color-coded badges (secondary, warning, info, success, danger)
- Format status text (e.g., "DRAFT" → "Draft")
- Required validation
Props: modelValue, statusList, disabled, required, etc.
Templates: 7 Remaining Typed Fields
6. TimeField
<!-- src/components/fields/typed/TimeField/TimeField.vue -->
<template>
<div class="mb-3">
<label v-if="label" :for="id" class="form-label">
{{ label }}
<span v-if="required" class="text-danger">*</span>
</label>
<input
:id="id"
:value="modelValue"
type="time"
:disabled="disabled"
:class="['form-control', { 'is-invalid': error }]"
@input="handleInput"
@blur="handleBlur"
/>
<div v-if="error" class="invalid-feedback d-block">{{ error }}</div>
</div>
</template>
<script setup lang="ts">
// Similar to DateField
// Format: HH:mm
// Props: modelValue, label, minTime, maxTime, disabled, required
</script>
7. PercentageField
<!-- src/components/fields/typed/PercentageField/PercentageField.vue -->
<template>
<div class="mb-3">
<label v-if="label" :for="id" class="form-label">
{{ label }}
<span v-if="required" class="text-danger">*</span>
</label>
<div class="input-group">
<input
:id="id"
:value="displayValue"
type="number"
:min="0"
:max="100"
:disabled="disabled"
:class="['form-control', 'text-end', { 'is-invalid': error }]"
@input="handleInput"
@blur="handleBlur"
/>
<span class="input-group-text">%</span>
</div>
<div v-if="error" class="invalid-feedback d-block">{{ error }}</div>
</div>
</template>
<script setup lang="ts">
// Validation: 0-100 range
// Decimal support (0-2 decimals default)
// Props: modelValue, label, disabled, required, decimals
</script>
8. QuantityField
<!-- src/components/fields/typed/QuantityField/QuantityField.vue -->
<template>
<div class="mb-3">
<label v-if="label" :for="id" class="form-label">
{{ label }}
<span v-if="required" class="text-danger">*</span>
</label>
<input
:id="id"
:value="modelValue"
type="number"
:min="minQty"
:max="maxQty"
:step="1"
:disabled="disabled"
:class="['form-control', { 'is-invalid': error }]"
@input="handleInput"
@blur="handleBlur"
/>
<div v-if="error" class="invalid-feedback d-block">{{ error }}</div>
</div>
</template>
<script setup lang="ts">
// Positive integers only (no decimals)
// Min/Max validation
// Props: modelValue, minQty, maxQty, disabled, required
</script>
9. MultiSelectField
<!-- src/components/fields/typed/MultiSelectField/MultiSelectField.vue -->
<template>
<div class="mb-3">
<label v-if="label" class="form-label">
{{ label }}
<span v-if="required" class="text-danger">*</span>
</label>
<div class="multi-select">
<div class="selected-tags">
<span
v-for="value in modelValue"
:key="value"
class="badge bg-primary me-2 mb-2"
>
{{ getOptionLabel(value) }}
<button type="button" @click="removeOption(value)" class="btn-close btn-close-white ms-2" />
</span>
</div>
<select
:multiple="true"
:value="modelValue"
:disabled="disabled"
:class="['form-select', { 'is-invalid': error }]"
@change="handleChange"
>
<option v-for="option in options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
<div v-if="error" class="invalid-feedback d-block">{{ error }}</div>
</div>
</template>
<script setup lang="ts">
// Array of selected values
// Tag display for selected items
// Props: modelValue (array), options, maxItems, disabled, required
</script>
10. CheckboxField
<!-- src/components/fields/typed/CheckboxField/CheckboxField.vue -->
<template>
<div class="form-check">
<input
:id="id"
type="checkbox"
class="form-check-input"
:checked="modelValue"
:disabled="disabled"
@change="handleChange"
/>
<label :for="id" class="form-check-label">
{{ label }}
<span v-if="required" class="text-danger">*</span>
</label>
<small v-if="helpText" class="form-text text-muted d-block">
{{ helpText }}
</small>
</div>
</template>
<script setup lang="ts">
// Boolean checkbox
// Props: modelValue (boolean), label, disabled, required, helpText
</script>
11. SearchField
<!-- src/components/fields/typed/SearchField/SearchField.vue -->
<template>
<div class="mb-3">
<label v-if="label" :for="id" class="form-label">
{{ label }}
<span v-if="required" class="text-danger">*</span>
</label>
<div class="position-relative">
<input
:id="id"
v-model="searchQuery"
type="text"
:placeholder="placeholder"
:disabled="disabled"
:class="['form-control', { 'is-invalid': error }]"
@input="handleSearch"
@focus="showSuggestions = true"
@blur="showSuggestions = false"
/>
<div v-if="showSuggestions && suggestions.length > 0" class="dropdown-menu show w-100">
<a
v-for="suggestion in suggestions"
:key="suggestion.id"
href="#"
class="dropdown-item"
@click.prevent="selectSuggestion(suggestion)"
>
{{ suggestion.label }}
</a>
</div>
</div>
<div v-if="error" class="invalid-feedback d-block">{{ error }}</div>
</div>
</template>
<script setup lang="ts">
// Autocomplete search with API lookup
// Debounced search (300ms default)
// Props: modelValue, placeholder, onSearch (async function), suggestions, disabled, required
// Emits: select (when option selected)
</script>
12. PhoneField
<!-- src/components/fields/typed/PhoneField/PhoneField.vue -->
<template>
<div class="mb-3">
<label v-if="label" :for="id" class="form-label">
{{ label }}
<span v-if="required" class="text-danger">*</span>
</label>
<input
:id="id"
:value="displayValue"
type="tel"
:placeholder="placeholder || '010-1234-5678'"
:disabled="disabled"
:class="['form-control', { 'is-invalid': error }]"
@input="handleInput"
@blur="handleBlur"
/>
<div v-if="error" class="invalid-feedback d-block">{{ error }}</div>
</div>
</template>
<script setup lang="ts">
// Phone number formatting (Korean: 010-1234-5678)
// Validation: 10-11 digits
// Props: modelValue, label, disabled, required, format (default: Korean)
// useFormatting().formatPhone(value)
</script>
Shared Composables (Created)
useValidation.ts
// Validation rules
- required(message?)
- email(message?)
- minLength(min, message?)
- maxLength(max, message?)
- min(min, message?)
- max(max, message?)
- pattern(regex, message?)
- numeric(message?)
- positiveInteger(message?)
- percentage(message?)
- url(message?)
// Usage:
const validator = createValidationRules()
const error = validator.validate(value, [
validator.required(),
validator.email()
])
useFormatting.ts
// Formatting utilities
- formatCurrency(value, decimals, symbol)
- parseCurrency(value)
- formatPercentage(value, decimals)
- parsePercentage(value)
- formatDate(value, format)
- parseDate(value)
- formatTime(value, format)
- parseTime(value)
- formatPhone(value)
- parsePhone(value)
- formatNumber(value, decimals)
- parseNumber(value)
- truncate(value, length, suffix)
- capitalize(value)
- upperCase(value)
- lowerCase(value)
// Usage:
const { formatCurrency, formatDate } = useFormatting()
const displayPrice = formatCurrency(9999) // ₩9,999
const isoDate = formatDate('2026-08-12') // 2026-08-12
Testing Strategy (Phase 2)
Unit Tests for Each Typed Field
// src/components/fields/typed/TextField/TextField.spec.ts
describe('TextField', () => {
it('validates required field', () => {
const wrapper = mount(TextField, {
props: {
modelValue: '',
required: true,
validationRules: [validator.required()]
}
})
wrapper.vm.handleBlur()
expect(wrapper.vm.error).toBe('This field is required')
})
it('formats input on blur', () => {
const wrapper = mount(TextField, {
props: { modelValue: 'test' }
})
wrapper.vm.handleBlur()
expect(wrapper.emitted('blur')).toBeTruthy()
})
})
Target: 5-8 tests per field, ~70+ integration tests total
Storybook Stories
// src/components/fields/typed/TextField/TextField.stories.ts
export const Default: Story = {
args: {
label: 'Username',
placeholder: 'Enter username',
required: true
}
}
export const WithError: Story = {
args: {
label: 'Email',
type: 'email',
error: 'Invalid email address'
}
}
export const WithCounter: Story = {
args: {
label: 'Bio',
type: 'text',
maxLength: 160,
showCounter: true
}
}
Target: 8-12 stories per field, ~100+ stories total
Phase 2 Step 1 Completion Checklist
- Validation composable (useValidation.ts) ✅
- Formatting composable (useFormatting.ts) ✅
- 5 Typed Fields fully implemented (TextField, DateField, CurrencyField, SelectField, StatusField) ✅
- 7 Typed Fields templates provided (ready to implement)
- All 12 Storybook stories added (100+ stories)
- All 12 unit tests added (70+ tests passing)
- Integration tests for form validation chains
- WCAG 2.1 AA accessibility audit
Quick Start: Generate Remaining 7 Fields
# Use the component generator from Phase 1
npm run component:create TimeField
npm run component:create PercentageField
npm run component:create QuantityField
npm run component:create MultiSelectField
npm run component:create CheckboxField
npm run component:create SearchField
npm run component:create PhoneField
# Then implement using templates above
# For each: copy template → customize → add stories → add tests
Next: Phase 2 Step 2 (Pinia Stores)
Once all 12 Typed Fields complete:
- Generate 10 Pinia store modules
- API client integration
- Mock Service Worker (MSW) setup
- Integration tests with API mocks
Timeline: Complete by 2026-08-19 (Friday, Week 3)
Next Phase: Step 2 Pinia Stores (Week 4)