15 KiB
Phase 3 Step 1: Complete Remaining Typed Fields (7 Components)
Status: 🚀 START
Phase: 3 / 11
Step: 1 / 4
Target Completion: 2026-08-28 (2 days)
Overall Phase 3: 2 weeks (2026-08-27 to 2026-09-10)
Overview
Phase 3 Goal: Build Smart Components & Domain Fields layers
Step 1 Focus: Complete Typed Fields layer (12/12 components total)
- ✅ Phase 2: 5 completed (TextField, DateField, CurrencyField, SelectField, StatusField)
- 🔄 Phase 3 Step 1: 7 remaining (NumberField, PercentageField, PhoneField, EmailField, URLField, TextareaField, CheckboxField)
After Step 1: Full Typed Fields layer ready for Domain Fields (Step 2)
The 7 Remaining Typed Fields
1. NumberField — Numeric input with min/max validation
<template>
<div class="form-group">
<label v-if="label">{{ label }}</label>
<input
type="number"
:value="modelValue"
:min="minValue"
:max="maxValue"
:step="step"
@input="$emit('update:modelValue', Number($event.target.value))"
/>
</div>
</template>
<script setup lang="ts">
interface Props {
modelValue: number
label?: string
minValue?: number
maxValue?: number
step?: number // default: 1
disabled?: boolean
required?: boolean
errorMessage?: string
}
withDefaults(defineProps<Props>(), {
step: 1
})
defineEmits<{
'update:modelValue': [value: number]
}>()
</script>
Validation Rules:
- required: number cannot be empty
- min: value >= minValue
- max: value <= maxValue
- integer: no decimals (if step=1)
Use Cases: Quantity, Age, Count
2. PercentageField — Percentage input (0-100)
<template>
<div class="input-group">
<input
type="number"
:value="modelValue"
min="0"
max="100"
step="0.01"
@input="$emit('update:modelValue', Number($event.target.value))"
/>
<span class="input-group-text">%</span>
</div>
</template>
<script setup lang="ts">
interface Props {
modelValue: number // 0-100
label?: string
disabled?: boolean
required?: boolean
decimals?: number // default: 2
}
withDefaults(defineProps<Props>(), {
decimals: 2
})
defineEmits<{
'update:modelValue': [value: number]
}>()
</script>
Validation Rules:
- required
- min: 0
- max: 100
- precision: decimal places
Use Cases: Discount %, Markup %, Tax Rate
3. PhoneField — Phone number with international format
<template>
<div class="form-group">
<label v-if="label">{{ label }}</label>
<input
type="tel"
:value="displayValue"
placeholder="+82 10 1234 5678"
@input="handleInput"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
interface Props {
modelValue: string
label?: string
countryCode?: string // default: 'KR'
disabled?: boolean
required?: boolean
}
const props = withDefaults(defineProps<Props>(), {
countryCode: 'KR'
})
defineEmits<{
'update:modelValue': [value: string]
}>()
const formatPhone = (value: string) => {
// Format: +82 10 1234 5678 (Korean)
// Remove non-digits
const digits = value.replace(/\D/g, '')
if (digits.length <= 2) return digits
if (digits.length <= 6) return `+${digits.slice(0, 2)} ${digits.slice(2)}`
return `+${digits.slice(0, 2)} ${digits.slice(2, 4)} ${digits.slice(4, 8)} ${digits.slice(8)}`
}
const displayValue = computed(() => formatPhone(props.modelValue))
const handleInput = (e: Event) => {
const value = (e.target as HTMLInputElement).value
// Store only digits
const digits = value.replace(/\D/g, '')
emit('update:modelValue', digits)
}
</script>
Validation Rules:
- required
- length: 10-15 digits
- pattern: valid phone format
- country-specific (KR, US, JP, etc.)
Use Cases: Customer Phone, Supplier Contact
4. EmailField — Email input with validation
<template>
<div class="form-group">
<label v-if="label">{{ label }}</label>
<input
type="email"
:value="modelValue"
placeholder="user@example.com"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
@blur="validateEmail"
/>
</div>
</template>
<script setup lang="ts">
interface Props {
modelValue: string
label?: string
disabled?: boolean
required?: boolean
helpText?: string
errorMessage?: string
}
defineProps<Props>()
defineEmits<{
'update:modelValue': [value: string]
blur: []
}>()
const validateEmail = (email: string) => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return regex.test(email)
}
</script>
Validation Rules:
- required
- email: valid email format
- length: max 254 chars (RFC 5321)
Use Cases: User Email, Customer Email
5. URLField — URL input with validation
<template>
<div class="form-group">
<label v-if="label">{{ label }}</label>
<input
type="url"
:value="modelValue"
placeholder="https://example.com"
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
</div>
</template>
<script setup lang="ts">
interface Props {
modelValue: string
label?: string
disabled?: boolean
required?: boolean
protocol?: string // default: 'https'
errorMessage?: string
}
withDefaults(defineProps<Props>(), {
protocol: 'https'
})
defineEmits<{
'update:modelValue': [value: string]
}>()
const validateURL = (url: string) => {
try {
new URL(url)
return true
} catch {
return false
}
}
</script>
Validation Rules:
- required
- url: valid URL format
- protocol: https, http, ftp
- length: max 2048 chars
Use Cases: Website URL, API Endpoint
6. TextareaField — Multi-line text input
<template>
<div class="form-group">
<label v-if="label">{{ label }}</label>
<textarea
:value="modelValue"
:placeholder="placeholder"
:rows="rows"
:maxlength="maxLength"
:disabled="disabled"
@input="$emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
@blur="$emit('blur')"
/>
<small v-if="showCounter" class="form-text">
{{ modelValue.length }} / {{ maxLength }}
</small>
</div>
</template>
<script setup lang="ts">
interface Props {
modelValue: string
label?: string
placeholder?: string
rows?: number // default: 4
maxLength?: number // default: 1000
disabled?: boolean
required?: boolean
showCounter?: boolean // default: true
helpText?: string
errorMessage?: string
}
withDefaults(defineProps<Props>(), {
rows: 4,
maxLength: 1000,
showCounter: true
})
defineEmits<{
'update:modelValue': [value: string]
blur: []
}>()
</script>
Validation Rules:
- required
- minLength: configurable
- maxLength: default 1000
- wordCount: optional limit
Use Cases: Description, Notes, Comments, Address
7. CheckboxField — Boolean checkbox with label
<template>
<div class="form-check">
<input
:id="`checkbox-${id}`"
type="checkbox"
:checked="modelValue"
class="form-check-input"
:disabled="disabled"
@change="$emit('update:modelValue', ($event.target as HTMLInputElement).checked)"
/>
<label :for="`checkbox-${id}`" class="form-check-label">
{{ label }}
<span v-if="required" class="text-danger">*</span>
</label>
<small v-if="helpText" class="form-text d-block">{{ helpText }}</small>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
interface Props {
modelValue: boolean
label: string
disabled?: boolean
required?: boolean
helpText?: string
}
defineProps<Props>()
defineEmits<{
'update:modelValue': [value: boolean]
}>()
const id = ref(`checkbox-${Math.random().toString(36).slice(2, 11)}`)
</script>
Validation Rules:
- required: must be checked
- value: true/false only
Use Cases: Terms & Conditions, Feature Toggles, Agreements
Implementation Strategy
Step 1.1: Create Field Components (2 hours)
# Create each field component
# File: src/components/fields/typed/[Field]/[Field].vue
# NumberField
src/components/fields/typed/NumberField/
├── NumberField.vue
├── NumberField.stories.ts (5+ stories)
└── NumberField.spec.ts (unit tests)
# PercentageField
src/components/fields/typed/PercentageField/
├── PercentageField.vue
├── PercentageField.stories.ts
└── PercentageField.spec.ts
# ... repeat for PhoneField, EmailField, URLField, TextareaField, CheckboxField
Step 1.2: Validation Rules (1 hour)
// src/composables/useValidation.ts - Add new rules
const rules = {
number: (min?, max?) => ({...}),
percentage: () => ({...}),
phone: (countryCode?) => ({...}),
email: () => ({...}),
url: () => ({...}),
textarea: (minLength?, maxLength?) => ({...}),
checkbox: () => ({...})
}
Step 1.3: Formatting Functions (1 hour)
// src/composables/useFormatting.ts - Add new formatters
const fmt = useFormatting()
fmt.formatPhone('01012345678') // "+82 10 1234 5678"
fmt.parsePhone('+82 10 1234 5678') // "01012345678"
fmt.formatPercentage(0.75, 2) // "75.00%"
Step 1.4: Storybook Documentation (1 hour)
// Each field: 5+ stories
// Example: NumberField.stories.ts
export default {
title: 'Fields/Typed/NumberField',
component: NumberField,
argTypes: {
minValue: { control: 'number' },
maxValue: { control: 'number' },
step: { control: 'number' }
}
}
export const Default = {...}
export const WithValidation = {...}
export const WithMinMax = {...}
export const Disabled = {...}
export const Error = {...}
Step 1.5: Unit Tests (1 hour)
# Each field: 10-15 unit tests
# Example: NumberField.spec.ts
describe('NumberField', () => {
it('validates min/max bounds')
it('formats decimal places correctly')
it('emits update events')
it('handles disable state')
it('shows error messages')
// ... etc
})
Implementation Timeline
| Task | Duration | Status |
|---|---|---|
| NumberField | 30 min | ⏳ Start |
| PercentageField | 30 min | ⏳ After NumberField |
| PhoneField | 40 min | ⏳ After PercentageField |
| EmailField | 30 min | ⏳ After PhoneField |
| URLField | 30 min | ⏳ After EmailField |
| TextareaField | 30 min | ⏳ After URLField |
| CheckboxField | 30 min | ⏳ After TextareaField |
| Total | 4.5 hours | ⏳ Start now |
Estimated Completion: 2026-08-28 (end of day)
Validation Matrix
| Field | Required | Phone | URL | Min/Max | Pattern | Custom | |
|---|---|---|---|---|---|---|---|
| Number | ✅ | — | — | — | ✅ | — | — |
| Percent | ✅ | — | — | — | ✅ (0-100) | — | — |
| Phone | ✅ | — | ✅ | — | — | ✅ | Country-specific |
| ✅ | ✅ | — | — | — | ✅ | Length limit | |
| URL | ✅ | — | — | ✅ | — | ✅ | Protocol check |
| Textarea | ✅ | — | — | — | ✅ | — | Word count |
| Checkbox | ✅ | — | — | — | — | — | Acceptance |
Story Examples
NumberField Stories (5+)
export const Default = Template.bind({})
Default.args = {
modelValue: 100,
label: 'Quantity',
minValue: 1,
maxValue: 9999
}
export const WithDecimals = Template.bind({})
WithDecimals.args = {
modelValue: 19.99,
label: 'Price',
step: 0.01,
minValue: 0
}
export const Disabled = Template.bind({})
Disabled.args = {
modelValue: 42,
disabled: true
}
export const Error = Template.bind({})
Error.args = {
modelValue: 5000,
errorMessage: 'Quantity cannot exceed 1000'
}
PhoneField Stories (5+)
export const Default = Template.bind({})
Default.args = {
modelValue: '01012345678',
label: 'Contact Phone'
}
export const Korea = Template.bind({})
Korea.args = {
modelValue: '01012345678',
countryCode: 'KR'
}
export const US = Template.bind({})
US.args = {
modelValue: '2025551234',
countryCode: 'US'
}
Testing Strategy
Unit Tests (Per Field: 10-15 tests)
// NumberField.spec.ts example
describe('NumberField', () => {
it('renders input with correct value')
it('emits update:modelValue on input')
it('validates min boundary')
it('validates max boundary')
it('handles decimal step')
it('shows error message when invalid')
it('respects disabled state')
it('focuses on click')
it('handles keyboard input')
it('handles paste event')
})
Storybook Visual Testing
npm run storybook
# Manually verify: rendering, validation, error states, accessibility
Integration Tests (New)
// tests/integration/typed-fields.spec.ts
describe('Typed Fields Integration', () => {
it('NumberField with validation rules')
it('PhoneField with formatting')
it('EmailField with API lookup')
it('URLField with protocol validation')
it('CheckboxField with toggle state')
})
Files to Create
Phase 3 Step 1 Deliverables:
src/components/fields/typed/
├── NumberField/
│ ├── NumberField.vue
│ ├── NumberField.stories.ts
│ └── NumberField.spec.ts
├── PercentageField/
│ ├── PercentageField.vue
│ ├── PercentageField.stories.ts
│ └── PercentageField.spec.ts
├── PhoneField/
│ ├── PhoneField.vue
│ ├── PhoneField.stories.ts
│ └── PhoneField.spec.ts
├── EmailField/
│ ├── EmailField.vue
│ ├── EmailField.stories.ts
│ └── EmailField.spec.ts
├── URLField/
│ ├── URLField.vue
│ ├── URLField.stories.ts
│ └── URLField.spec.ts
├── TextareaField/
│ ├── TextareaField.vue
│ ├── TextareaField.stories.ts
│ └── TextareaField.spec.ts
└── CheckboxField/
├── CheckboxField.vue
├── CheckboxField.stories.ts
└── CheckboxField.spec.ts
tests/integration/
└── typed-fields.spec.ts (new integration tests)
src/composables/
├── useValidation.ts (updated: new rules)
└── useFormatting.ts (updated: new formatters)
Documentation/
└── PHASE3-STEP1-COMPLETION.md (completion checklist)
Quality Checklist (Per Field)
- Component renders correctly
- Props are typed (no
any) - Emits work (update:modelValue, blur)
- Validation rules integrated
- Formatting applied
- Error messages display
- Disabled state respected
- Accessibility (labels, ARIA, keyboard)
- 5+ Storybook stories
- 10+ unit tests passing
- TypeScript strict: 0 errors
- No console warnings
Exit Criteria (Step 1 Complete)
- ✅ All 7 fields implemented
- ✅ All 35+ Storybook stories created
- ✅ All 70+ unit tests passing
- ✅ Validation + formatting integrated
- ✅ TypeScript strict: 0 errors
- ✅
npm run verifypasses - ✅ Integration tests added
- ✅ PHASE3-STEP1-COMPLETION.md filled
Next Phase (After Step 1)
Step 2: Smart Components Layer (Domain Fields)
- OrderLineField (with Product lookup)
- CustomerField (with Customer lookup)
- ProductField (with SKU validation)
- WarehouseField
- etc.
Ready to implement Phase 3 Step 1? ✅
Estimated time: 4.5 hours
Target completion: 2026-08-28 (end of day)
Proceed with NumberField implementation? → Yes ✅