try modularizing validated form input(s)

This commit is contained in:
einhornimmond 2025-02-06 16:18:32 +01:00
parent 851db7e8c5
commit 6bbb97048e
7 changed files with 174 additions and 101 deletions

View File

@ -9,12 +9,11 @@
id="contribution-date"
:model-value="formValues.date"
name="date"
:state="dataFieldMeta.valid"
:label="$t('contribution.selectDate')"
:no-flip="true"
class="mb-4 bg-248"
type="date"
:schema-description="schemaDescription.fields.date"
:rules="validationSchema"
@update:model-value="updateField"
/>
<div v-if="showMessage" class="p-3" data-test="contribtion-message">
@ -23,33 +22,33 @@
<div v-else>
<input-textarea
id="contribution-memo"
:model-value="formValues.memo"
name="memo"
:label="$t('contribution.activity')"
:placeholder="$t('contribution.yourActivity')"
:rules="{ required: true, min: 5, max: 255 }"
:rules="validationSchema"
@update:model-value="updateField"
/>
<input-hour
<ValidatedInput
name="hours"
:model-value="formValues.hours"
:label="$t('form.hours')"
placeholder="0.01"
:rules="{
required: true,
// decimal: 2,
min: 0.01,
max: validMaxTime,
}"
:valid-max-time="validMaxTime"
step="0.01"
type="number"
:rules="validationSchema"
@update:model-value="updateField"
/>
<input-amount
<LabeledInput
id="contribution-amount"
class="mt-3"
name="amount"
:label="$t('form.amount')"
placeholder="20"
:rules="{ required: true, gddSendAmount: { min: 20, max: validMaxGDD } }"
typ="ContributionForm"
readonly
type="text"
trim
/>
<BRow class="mt-5">
<BCol>
<BButton
@ -80,14 +79,14 @@
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import InputHour from '@/components/Inputs/InputHour'
import InputAmount from '@/components/Inputs/InputAmount'
import InputTextarea from '@/components/Inputs/InputTextarea'
import ValidatedInput from '@/components/Inputs/ValidatedInput.vue'
import { createContributionFormValidation } from '@/validationSchemas'
import { useForm, useField } from 'vee-validate'
import ValidatedInput from '@/components/Inputs/ValidatedInput'
import LabeledInput from '@/components/Inputs/LabeledInput'
import { memo } from '@/validationSchemas'
import { useForm } from 'vee-validate'
import { object, date, number } from 'yup'
const props = defineProps({
modelValue: { type: Object, required: true },
@ -103,10 +102,34 @@ const { t } = useI18n()
const form = ref({ ...props.modelValue })
const validationSchema = createContributionFormValidation(t)
const schemaDescription = validationSchema.describe()
// console.log(schemaDescription)
const maxHours = computed(() => Number(props.isThisMonth ? props.maxGddThisMonth : props.maxGddLastMonth / 20))
const validationSchema = computed(() => {
// const maxGDD = Number(props.isThisMonth ? props.maxGddThisMonth : props.maxGddLastMonth)
// const maxHours = Number(maxGDD / 20)
console.log('compute validationSchema', { maxHours })
return object({
// The date field is required and needs to be a valid date
// contribution date
date: date()
.required('contribution.noDateSelected')
.min(new Date(new Date().setMonth(new Date().getMonth() - 1, 1)).toISOString().slice(0, 10)) // min date is first day of last month
.max(new Date().toISOString().slice(0, 10))
.default(''), // date cannot be in the future
memo,
hours: number().required('contribution.noHours')
.min(0.01, ({min}) => ({ key: 'form.validation.gddCreationTime.min', values: { min } }))
.max(maxHours.value, ({max}) => ({ key: 'form.validation.gddCreationTime.max', values: { max } }))
.test(
'decimal-places',
'form.validation.gddCreationTime.decimal-places',
(value) => {
if (value === undefined || value === null) return true
return /^\d+(\.\d{0,2})?$/.test(value.toString())
}
),
})
})
const {
values: formValues,
meta: formMeta,
@ -119,15 +142,16 @@ const {
hours: props.modelValue.hours,
amount: props.modelValue.amount,
},
validationSchema,
validationSchema: validationSchema.value,
})
const { meta: dataFieldMeta } = useField('date')
const updateField = (newValue, name) => {
if (typeof name === 'string' && name.length) {
setFieldValue(name, newValue)
}
if (name === 'hours') {
setFieldValue('amount', (newValue * 20).toFixed(2).toString())
}
}
const showMessage = computed(() => {
@ -148,14 +172,6 @@ const disabled = computed(() => {
)
})
const validMaxGDD = computed(() => {
return Number(props.isThisMonth ? props.maxGddThisMonth : props.maxGddLastMonth)
})
const validMaxTime = computed(() => {
return Number(validMaxGDD.value / 20)
})
const noOpenCreation = computed(() => {
if (props.maxGddThisMonth <= 0 && props.maxGddLastMonth <= 0) {
return t('contribution.noOpenCreation.allMonth')
@ -169,17 +185,6 @@ const noOpenCreation = computed(() => {
return ''
})
watch(
() => formValues.hours,
() => {
updateAmount(formValues.hours)
},
)
function updateAmount(hours) {
setFieldValue('amount', (hours * 20).toFixed(2).toString())
}
function submit() {
const dataToSave = { ...formValues }
let emitOption = 'set-contribution'
@ -194,12 +199,11 @@ function submit() {
function fullFormReset() {
resetForm({
values: {
id: null,
date: '',
memo: '',
hours: 0,
hours: 0.0,
amount: '',
},
}
})
}
</script>

View File

@ -16,7 +16,7 @@
@update:modelValue="currentValue = $event"
/>
<BFormInvalidFeedback v-if="errorMessage">
{{ errorMessage }}
{{ translatedErrorString }}
</BFormInvalidFeedback>
</BFormGroup>
</div>
@ -25,6 +25,8 @@
<script setup>
import { computed } from 'vue'
import { useField } from 'vee-validate'
import { useI18n } from 'vue-i18n'
import { isLanguageKey } from '@/validationSchemas'
const props = defineProps({
rules: {
@ -50,7 +52,17 @@ const props = defineProps({
})
const { value: currentValue, errorMessage, meta } = useField(props.name, props.rules)
const { t } = useI18n()
const translatedErrorString = computed(() => {
console.log(errorMessage)
if (typeof errorMessage.value === 'object') {
return t(errorMessage.value.key, errorMessage.value.values)
} else if (isLanguageKey(errorMessage.value)) {
return t(errorMessage.value)
} else {
return errorMessage
}
})
const labelFor = computed(() => `${props.name}-input-field`)
</script>

View File

@ -0,0 +1,32 @@
<template>
<div :class="wrapperClassName">
<BFormGroup :label="label" :label-for="labelFor">
<BFormInput
v-bind="$attrs"
/>
<slot></slot>
</BFormGroup>
</div>
</template>
<script setup>
import { computed, defineOptions } from 'vue'
defineOptions({
inheritAttrs: false,
})
const props = defineProps({
label: {
type: String,
required: true,
},
name: {
type: String,
required: true,
},
})
const wrapperClassName = computed(() => `input-${props.name}`)
const labelFor = computed(() => `${props.name}-input-field`)
</script>

View File

@ -1,58 +1,73 @@
<template>
<div :class="wrapperClassName">
<BFormGroup :label="label" :label-for="labelFor">
<BFormInput
v-bind="$attrs"
:min="minValue"
:max="maxValue"
:model-value="modelValue"
:reset-value="props.schemaDescription.default"
:locale="$i18n.locale"
:required="!props.schemaDescription.optional"
@input="updateValue($event.target.value)"
/>
<BFormInvalidFeedback v-if="errorMessage">
{{ props.errorMessage }}
</BFormInvalidFeedback>
</BFormGroup>
</div>
<LabeledInput
v-bind="$attrs"
:min="minValue"
:max="maxValue"
:model-value="currentValue"
:reset-value="resetValue"
:locale="$i18n.locale"
:required="!isOptional"
:label="props.label"
:name="props.name"
:state="meta.valid"
@input="updateValue($event.target.value)">
<BFormInvalidFeedback v-if="errorMessage">
{{ $t(translatedErrorString) }}
</BFormInvalidFeedback>
</LabeledInput>
</template>
<script setup>
import { computed, defineOptions } from 'vue'
defineOptions({
inheritAttrs: false,
})
import { computed } from 'vue'
import LabeledInput from './LabeledInput'
import { useI18n } from 'vue-i18n'
import { isLanguageKey } from '@/validationSchemas'
import { useField } from 'vee-validate'
const props = defineProps({
errorMessage: {
type: String,
required: false,
},
errorMessage: [String, Object],
label: {
type: String,
required: true,
},
modelValue: {
modelValue: [String, Number],
name: {
type: String,
required: true,
},
name: [String, Number],
schemaDescription: {
rules: {
type: Object,
required: true,
default: () => ({}),
},
})
const emit = defineEmits('update:modelValue')
const { t } = useI18n()
const translatedErrorString = computed(() => {
const error = errorMessage.value
const type = typeof error
console.log(error)
if (type === 'object') {
return t(error.key, error.values)
} else if (type === 'string' && error.length > 0 && isLanguageKey(error)) {
return t(error)
} else {
return error
}
})
const emit = defineEmits(['update:modelValue'])
const updateValue = (newValue) => emit('update:modelValue', newValue, props.name)
// extract additional parameter like min and max from schema
const getDateOnly = (rules, name) => rules.find((test) => test.name === name)?.params[name]
const getDateOnly = (schemaDescription, name) => schemaDescription.fields[props.name].tests.find((test) => test.name === name)?.params[name]
const minValue = computed(() => getDateOnly(props.schemaDescription.tests, 'min'))
const maxValue = computed(() => getDateOnly(props.schemaDescription.tests, 'max'))
const wrapperClassName = computed(() => `input-${props.name}`)
const labelFor = computed(() => `${props.name}-input-field`)
const schemaDescription = computed(() => props.rules.describe())
console.log(schemaDescription.value)
const minValue = computed(() => getDateOnly(schemaDescription.value, 'min'))
const maxValue = computed(() => getDateOnly(schemaDescription.value, 'max'))
const resetValue = computed(() => schemaDescription.value.fields[props.name].default)
const isOptional = computed(() => schemaDescription.value.fields[props.name].optional)
const { value: currentValue, errorMessage, meta } = useField(props.name, props.rules)
console.log('max value: ', maxValue)
</script>

View File

@ -77,6 +77,7 @@
"myContributions": "Du hast noch keine Beiträge eingereicht."
},
"noDateSelected": "Wähle irgendein Datum im Monat",
"noHours": "Bitte trage deine Stunden ein",
"noOpenCreation": {
"allMonth": "Für alle beiden Monate ist dein Schöpfungslimit erreicht. Den Nächsten Monat kannst du wieder 1000 GDD Schöpfen.",
"lastMonth": "Für den ausgewählten Monat ist das Schöpfungslimit erreicht.",
@ -185,9 +186,17 @@
"username": "Benutzername",
"username-placeholder": "Wähle deinen Benutzernamen",
"validation": {
"gddCreationTime": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens einer Nachkommastelle sein",
"gddCreationTime": {
"min": "Die Stunden sollten mindestens {min} groß sein",
"max": "Die Stunden sollten höchsten {max} groß sein",
"decimal-places": "Die Stunden sollten maximal zwei Nachkommastellen enthalten"
},
"gddSendAmount": "Das Feld {_field_} muss eine Zahl zwischen {min} und {max} mit höchstens zwei Nachkommastellen sein",
"is-not": "Du kannst dir selbst keine Gradidos überweisen",
"memo": {
"min": "Die Tätigkeitsbeschreibung sollte mindestens {min} Zeichen lang sein",
"max": "Die Tätigkeitsbeschreibung sollte höchstens {max} Zeichen lang sein"
},
"requiredField": "{fieldName} ist ein Pflichtfeld",
"username-allowed-chars": "Der Nutzername darf nur aus Buchstaben (ohne Umlaute), Zahlen, Binde- oder Unterstrichen bestehen.",
"username-hyphens": "Binde- oder Unterstriche müssen zwischen Buchstaben oder Zahlen stehen.",

View File

@ -77,6 +77,7 @@
"myContributions": "You have not submitted any entries yet."
},
"noDateSelected": "Choose any date in the month",
"noHours": "Please enter your hours",
"noOpenCreation": {
"allMonth": "For all two months your creation limit is reached. The next month you can create 1000 GDD again.",
"lastMonth": "The creation limit is reached for the selected month.",
@ -188,6 +189,10 @@
"gddCreationTime": "The field {_field_} must be a number between {min} and {max} with at most one decimal place.",
"gddSendAmount": "The {_field_} field must be a number between {min} and {max} with at most two digits after the decimal point",
"is-not": "You cannot send Gradidos to yourself",
"memo": {
"min": "The job description should be at least {min} characters long",
"max": "The job description should not be longer than {max} characters"
},
"requiredField": "The {fieldName} field is required",
"username-allowed-chars": "The username may only contain letters, numbers, hyphens or underscores.",
"username-hyphens": "Hyphens or underscores must be in between letters or numbers.",

View File

@ -1,14 +1,10 @@
import { object, string, date } from 'yup'
import { string } from 'yup'
// TODO: only needed for grace period, before all inputs updated for using veeValidate + yup
export const isLanguageKey = (str) => str.match(/^(?!\.)[a-z][a-zA-Z0-9]*([.][a-z][a-zA-Z0-9]*)*(?<!\.)$/)
export const memo = string()
.required('contribution.yourActivity')
.min(5, ({min}) => ({ key: 'form.validation.memo.min', values: { min } }))
.max(255, ({max}) => ({ key: 'form.validation.memo.max', values: { max } }))
export const createContributionFormValidation = (t) => {
return object({
// The date field is required and needs to be a valid date
// contribution date
date: date()
.required(t('contribution.noDateSelected'))
.min(new Date(new Date().setMonth(new Date().getMonth() - 1, 1)).toISOString().slice(0, 10)) // min date is first day of last month
.max(new Date().toISOString().slice(0, 10))
.default(''), // date cannot be in the future
memo: string().required(t('')).min(5).max(255),
})
}