Skip to content

Commit 554b445

Browse files
committed
fix: broken form input validation
1 parent 9367869 commit 554b445

File tree

16 files changed

+192
-122
lines changed

16 files changed

+192
-122
lines changed

.github/workflows/workflow.yml renamed to .github/workflows/issues.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ on:
44
push:
55
branches: [dev]
66
workflow_dispatch:
7-
branches: [dev]
87
inputs:
98
MANUAL_COMMIT_REF:
109
description: "The SHA of the commit to get the diff for"

packages/common-helpers/src/form/composable.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@ export default function useForm(options: iPluginOptions = {}) {
3939
async function getResponse<R, RV extends Record<string, any> = Record<string, any>>(
4040
request: tResponseFn<R, RV>,
4141
inputs: RV | FormInput[] = [],
42-
event?: Event
42+
event?: Event,
43+
plainValues = true
4344
): Promise<iFormResponse<R, HTMLElement | string>> {
44-
const { values, invalidInputs } = getFormValues<RV>(inputs);
45+
const { values, invalidInputs } = getFormValues<RV>(inputs, plainValues);
4546
const modalTarget = (event?.target as HTMLElement)?.closest("dialog") || "body";
4647
let errors;
4748
let requestHadErrors = false;

packages/common-helpers/src/form/utils.ts

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,39 +42,63 @@ export function isValidValue<V extends iFormValue = iFormValue>(
4242
// field not empty, validate
4343
switch (input.type) {
4444
case eFormType.PHONE:
45-
// TODO: improve phone validation
45+
// TODO: improve phone & cellphone validation
4646
return Array.isArray(value) && value[1].toString().length >= 7;
4747
case eFormType.CELLPHONE:
48-
// TODO: improve cellphone validation
4948
return Array.isArray(value) && value[1].toString().length === 10;
5049
case eFormType.NEW_PASSWORD:
5150
return Array.isArray(value) && value[0] === value[1];
5251
case eFormType.EMAIL:
5352
return typeof value === "string" && isEmail(value);
5453
default:
54+
// no validation required, assume true
5555
return true;
5656
}
5757
}
5858

5959
/**
60-
* check if FormInput value is valid
60+
* Check if FormInput value is valid
61+
*
62+
* Array.every is truthy for empty arrays
6163
*/
62-
export const isValidFormInputValue = (input: FormInput, ignoreRequiredParam = false): boolean => {
63-
const { values, multiple, required, min, max } = input;
64+
export const isValidFormInputValue = (input: FormInput, ignoreRequired = false): boolean => {
65+
const { values, multiple, type, required, min, max } = input;
6466

65-
if (!values) return false;
67+
// When required, false if empty array
68+
if ((!values || !values.length) && required && !ignoreRequired) return false;
6669

6770
if (multiple) {
68-
if (values.length < min) return false;
69-
if (values.length > max) return false;
71+
// Bypass if not required
72+
// The UI should ensure the limits are not surpased
73+
if (required && !ignoreRequired) {
74+
if (values.length < min) return false;
75+
if (values.length > max) return false;
76+
}
77+
} else {
78+
if (type === eFormType.NUMBER) {
79+
// Number in range
80+
return values.every((number) => {
81+
number = Number(number);
82+
83+
return number >= min && number <= max;
84+
});
85+
} else if (!type || type === eFormType.TEXT) {
86+
// String length in range
87+
return values.every((string) => {
88+
const length = String(string).length;
89+
90+
return !string || (length >= min && length <= max);
91+
});
92+
}
7093
}
7194

72-
// value is valid or not
73-
const valid = !!values.length && values.every((value) => isValidValue(value, input));
95+
// The actual values are valid
96+
const valid = values.every((value) => isValidValue(value, input));
97+
// All values have content
7498
const notEmpty = values.every((v) => notEmptyValue(v, input.defaults));
7599

76100
// if empty but not required then value is truthy
77-
return valid || (!notEmpty && !required && !ignoreRequiredParam);
101+
return valid || (!notEmpty && (!required || ignoreRequired));
78102
};
79103

80104
/** suffixes used on values */
@@ -120,10 +144,13 @@ export function getFormInputsValues<V extends Record<string, any>>(
120144
): V {
121145
return inputs.reduce((acc, input, index) => {
122146
// inadecuate format, ignore
123-
if (!input.name || !input.values || !Array.isArray(input.values) || !input.values.length) {
124-
if (!input.name) console.log(`Missing name on input with index ${index}`);
125-
126-
return acc;
147+
if (!input.name) throw new Error(`Missing name property on input with index ${index}`);
148+
else if (!Array.isArray(input.values) || !input.values.length) {
149+
/**
150+
* Validation expects an array with at least one element
151+
* SUGGESTION: reconsider this approach
152+
*/
153+
if (input.required) input.values = [""];
127154
}
128155

129156
input.values.forEach((value) => {
@@ -197,7 +224,8 @@ export function getFormInputsValues<V extends Record<string, any>>(
197224
* Returns the actual data object to send to the api
198225
*/
199226
export function getFormValues<V extends Record<string, any>>(
200-
inputs: V | FormInput[]
227+
inputs: V | FormInput[],
228+
plainValues = true
201229
): iFormResults<V> {
202230
if (!Array.isArray(inputs)) return { values: inputs, invalidInputs: [] };
203231

@@ -212,7 +240,7 @@ export function getFormValues<V extends Record<string, any>>(
212240
})
213241
);
214242

215-
const values: V = getFormInputsValues(inputs);
243+
const values: V = getFormInputsValues(inputs, plainValues);
216244
const invalidInputs = getFormInputsInvalids(inputs).filter(({ name }) => {
217245
return values[name] !== undefined;
218246
});

packages/common-helpers/src/locale/en.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export const localeForm: iLocaleForm = {
104104
form_required_options: "Options are required",
105105
form_requires_n_values:
106106
"No values are required | A value is required | {count} values are required",
107+
form_loading_countries: "Loading countries...",
107108
form_country: "Look for country",
108109
form_state: "Look for state",
109110
form_city: "Look for city",
@@ -117,7 +118,8 @@ export const localeForm: iLocaleForm = {
117118
form_id_number: "ID number",
118119
form_complete_the_field: "Complete the field",
119120
form_location: "Location",
120-
form_required_field: "This field is required",
121+
form_invalid_field: "This field is invalid, fill it properly",
122+
form_required_field: "This field is required and can't be empty",
121123
form_use_valid_email: "You should use a valid E-mail address",
122124
form_use_valid_phone: "Too short. Use a valid phone number",
123125
form_use_valid_cellphone: "You should use a valid cellphone number",

packages/common-helpers/src/locale/es.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export const localeForm: iLocaleForm = {
105105
form_required_options: "Las opciones son requeridas",
106106
form_requires_n_values:
107107
"No se requieren valores | Se requiere un valor | {count} valores son requeridos",
108+
form_loading_countries: "Cargando paises...",
108109
form_country: "Buscar país",
109110
form_state: "Buscar provincia",
110111
form_city: "Buscar ciudad",
@@ -118,7 +119,8 @@ export const localeForm: iLocaleForm = {
118119
form_id_number: "Numero de identificacion",
119120
form_complete_the_field: "Completa el campo",
120121
form_location: "Ubicacion",
121-
form_required_field: "Este campo es requerido",
122+
form_invalid_field: "Este campo es invalido, completalo adecuadamente",
123+
form_required_field: "Este campo es requerido y no puede estar vacio",
122124
form_use_valid_email: "Debes usar una direccion de correo electronico valida",
123125
form_use_valid_phone: "Muy corto. Usa un numero de telefono valido",
124126
form_use_valid_cellphone: "Debes usar un numero de celular valido",

packages/common-types/src/locale.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,8 @@ export interface iLocaleForm {
144144
form_required_options: string;
145145
/** @example "No values are required | A value is required | {count} values are required" */
146146
form_requires_n_values: string;
147+
/** @example "Loading countries..." */
148+
form_loading_countries: string;
147149
/** @example "Look for country" */
148150
form_country: string;
149151
/** @example "Look for state" */
@@ -170,7 +172,9 @@ export interface iLocaleForm {
170172
form_complete_the_field: string;
171173
/** @example "Location" */
172174
form_location: string;
173-
/** @example "This field is required" */
175+
/** @example "This field is invalid, fill it properly" */
176+
form_invalid_field: string;
177+
/** @example "This field is required and can't be empty" */
174178
form_required_field: string;
175179
/** @example "You should use a valid E-mail address" */
176180
form_use_valid_email: string;

packages/components-vue/src/components/base/Wrapper.vue

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<template>
2-
<component :is="el" v-if="wrap" v-bind="$attrs">
3-
<slot></slot>
2+
<component :is="el" v-if="wrap" v-slot="elSlots" v-bind="{ ...$attrs, ...$slots }">
3+
<slot v-bind="{ ...elSlots }"></slot>
44
</component>
55
<slot v-else></slot>
66
</template>
@@ -42,4 +42,8 @@
4242
default: "div",
4343
},
4444
});
45+
/**
46+
* TODO: improve type safety for scoped slots in wrapper
47+
*/
48+
defineSlots<{ default(v: Record<string, any>): Record<string, any> }>();
4549
</script>

packages/components-vue/src/components/form/Input.vue

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@
4343
:theme="theme"
4444
:disabled="readonly"
4545
class="--flx"
46-
:min="input.min"
47-
:max="input.max"
4846
:file-prefix="_.snakeCase(input.placeholder)"
4947
:model-value="modelValue"
48+
:invalid="isInvalidByValidation"
49+
v-bind="inputProps"
5050
@update:model-value="$emit('update:model-value', $event)"
5151
/>
5252
<!-- Future inner loop input -->
@@ -97,6 +97,7 @@
9797
<InputText
9898
v-model="models[i].value[0]"
9999
v-bind="inputProps"
100+
:invalid="isInvalidByValidation"
100101
:theme="theme"
101102
:disabled="readonly"
102103
:placeholder="getInputPlaceholder()"
@@ -106,9 +107,10 @@
106107
<InputText
107108
v-model="models[i].value[1]"
108109
v-bind="inputProps"
110+
:invalid="isInvalidByValidation"
109111
:theme="theme"
110112
:disabled="readonly"
111-
:placeholder="getInputPlaceholder()"
113+
:placeholder="getInputPlaceholder(1)"
112114
type="password"
113115
class="--width-180 --flx"
114116
/>
@@ -127,6 +129,7 @@
127129
<InputText
128130
v-model="models[i].value[1]"
129131
v-bind="inputProps"
132+
:invalid="isInvalidByValidation"
130133
:theme="theme"
131134
:disabled="readonly"
132135
:placeholder="getInputPlaceholder()"
@@ -149,6 +152,7 @@
149152
<InputText
150153
v-model="models[i].value[1]"
151154
v-bind="inputProps"
155+
:invalid="isInvalidByValidation"
152156
:theme="theme"
153157
:disabled="readonly"
154158
:placeholder="getInputPlaceholder()"
@@ -159,7 +163,8 @@
159163
<FormInputCountriesAPI
160164
v-else-if="input.type === eFT.LOCATION"
161165
v-slot="{ statesReq, citiesReq }"
162-
:states="statesArr"
166+
:states="states"
167+
:theme="theme"
163168
:model="models[i].value"
164169
:values="[1, 3]"
165170
>
@@ -181,7 +186,7 @@
181186
>
182187
<SelectFilter
183188
v-model="models[i].value[1]"
184-
:options="statesArr || statesReq.content.map(stateToOption)"
189+
:options="statesArr || statesReq?.content?.map(stateToOption)"
185190
name="state"
186191
icon="mountain-sun"
187192
:theme="theme"
@@ -237,6 +242,7 @@
237242
:is="input.type === eFT.SELECT ? SelectSimple : SelectFilter"
238243
v-model="models[i].value"
239244
v-bind="inputProps"
245+
:invalid="isInvalidByValidation"
240246
:theme="theme"
241247
:disabled="readonly"
242248
:placeholder="input.placeholder"
@@ -261,18 +267,21 @@
261267
? { textarea: true }
262268
: { type: getInputTextType() }),
263269
}"
270+
:invalid="isInvalidByValidation"
264271
:theme="theme"
265272
:disabled="readonly"
266273
:placeholder="getInputPlaceholder()"
267274
class="--flx"
268275
/>
269276
</FormInputLoop>
270-
<p
271-
v-if="input.required && !notEmpty && isInvalidInput"
272-
class="--txtColor-danger --txtSize-sm"
273-
>
274-
{{ t("form_required_field") }}
275-
</p>
277+
<template v-if="isInvalidByProps">
278+
<p v-if="input.required && !notEmpty" class="--txtColor-danger --txtSize-sm">
279+
{{ t("form_required_field") }}
280+
</p>
281+
<p v-else class="--txtColor-danger --txtSize-sm">
282+
{{ t("form_invalid_field") }}
283+
</p>
284+
</template>
276285
</div>
277286
</template>
278287
<script setup lang="ts">
@@ -352,21 +361,22 @@
352361
const notEmpty = computed(() => {
353362
const values = props.input.values;
354363
355-
return values.every((v) => notEmptyValue(v, props.input.defaults));
364+
return !!values.length && values.every((v) => notEmptyValue(v, props.input.defaults));
356365
});
357-
const isInvalidInput = computed<boolean>(() => {
358-
const values = props.input.values;
359-
const byProp = !!props.invalid && _.isEqual(props.invalid.invalidValue, values);
360-
const byValidation = notEmpty.value && !isValidFormInputValue(props.input);
366+
const isInvalidByProps = computed<boolean>(() => {
367+
/** Validation expects an array with at least one element */
368+
const values = props.input.values.length ? props.input.values : [""];
361369
362-
return byProp || byValidation;
370+
return _.isEqual(props.invalid?.invalidValue, values);
371+
});
372+
const isInvalidByValidation = computed<boolean>(() => {
373+
return isInvalidByProps.value || !isValidFormInputValue(props.input, true);
363374
});
364375
const inputProps = computed(() => {
365376
const [icon, iconProps] = props.input?.icon || [];
366377
367378
return {
368379
..._.omit(props.input, ["type"]),
369-
invalid: isInvalidInput.value,
370380
autocomplete: getInputAutocomplete(),
371381
icon,
372382
iconProps,

packages/components-vue/src/components/form/InputCountriesAPI.vue

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,43 @@
11
<template>
2-
<FormInputNValues :model="model" :values="[1, 3]">
3-
<LoaderContentFetch
2+
<FormInputNValues :key="states?.length" :model="model" :values="[1, 3]">
3+
<BaseWrapper
44
v-slot="statesReq"
5+
:el="LoaderContentFetch"
6+
:wrap="!states && !!countryValue"
57
:theme="theme"
6-
:promise="!states && !!countryValue && unHydrate(getCountryStates)"
8+
:promise="unHydrate(getCountryStates)"
79
:payload="[countryValue]"
8-
:fallback="[]"
910
unwrap
1011
>
1112
<LoaderContentFetch
1213
v-slot="citiesReq"
1314
:theme="theme"
1415
:promise="!!model[1] && unHydrate(getStateCities)"
1516
:payload="[countryValue, model[1]]"
16-
:fallback="[]"
1717
unwrap
1818
>
1919
<slot v-bind="{ statesReq, citiesReq }"></slot>
2020
</LoaderContentFetch>
21-
</LoaderContentFetch>
21+
</BaseWrapper>
2222
</FormInputNValues>
2323
</template>
2424

2525
<script setup lang="ts">
2626
import { computed } from "vue";
2727
28-
import type { iSelectOption } from "@open-xamu-co/ui-common-types";
29-
28+
import BaseWrapper from "../base/Wrapper.vue";
3029
import LoaderContentFetch from "../loader/ContentFetch.vue";
3130
// input helper components
3231
import FormInputNValues from "./InputNValues.vue";
3332
3433
import type { iUseThemeProps } from "../../types/props";
34+
import type { iState } from "../../types/countries";
3535
import useFetch from "../../composables/fetch";
3636
import useCountries from "../../composables/countries";
3737
3838
interface iFormInputCountriesApi extends iUseThemeProps {
3939
model: string[];
40-
states?: iSelectOption[];
40+
states?: iState[];
4141
}
4242
4343
/**

0 commit comments

Comments
 (0)