Skip to content

Commit

Permalink
Created the AppInput client-validated component.
Browse files Browse the repository at this point in the history
  • Loading branch information
Utar94 committed Apr 18, 2024
1 parent c5833eb commit b3187f6
Show file tree
Hide file tree
Showing 5 changed files with 168 additions and 5 deletions.
8 changes: 4 additions & 4 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"@vee-validate/i18n": "~4.9.6",
"@vee-validate/rules": "~4.9.6",
"bootstrap": "^5.3.3",
"logitar-vue3-ui": "^2.0.5",
"logitar-vue3-ui": "^2.2.0",
"nanoid": "^5.0.7",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.1",
Expand Down
110 changes: 110 additions & 0 deletions frontend/src/components/shared/AppInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<script setup lang="ts">
import { TarInput, inputUtils, parsingUtils, type InputOptions, type InputStatus } from "logitar-vue3-ui";
import { computed, ref } from "vue";
import { nanoid } from "nanoid";
import { useField } from "vee-validate";
import { useI18n } from "vue-i18n";
import type { ValidationListeners, ValidationRules } from "@/types/validation";
const { isDateTimeInput, isNumericInput, isTextualInput } = inputUtils;
const { parseNumber } = parsingUtils;
const { t } = useI18n();
const props = withDefaults(
defineProps<
InputOptions & {
rules?: ValidationRules;
}
>(),
{
id: () => nanoid(),
},
);
const inputRef = ref<InstanceType<typeof TarInput> | null>(null);
const describedBy = computed<string>(() => `${props.id}_invalid-feedback`);
const inputMax = computed<number | string | undefined>(() => (isDateTimeInput(props.type) ? props.max : undefined));
const inputMin = computed<number | string | undefined>(() => (isDateTimeInput(props.type) ? props.min : undefined));
const inputName = computed<string>(() => props.name ?? props.id);
const validationRules = computed<ValidationRules>(() => {
const rules: ValidationRules = {};
if (props.required) {
rules.required = true;
}
if (isNumericInput(props.type)) {
if (props.max) {
rules.max_value = parseNumber(props.max);
}
if (props.min) {
rules.min_value = parseNumber(props.min);
}
} else if (isTextualInput(props.type)) {
if (props.max) {
rules.max_length = parseNumber(props.max);
}
if (props.min) {
rules.min_length = parseNumber(props.min);
}
}
if (props.type === "email" || props.type === "url") {
rules[props.type] = true;
}
return { ...rules, ...props.rules };
});
const displayLabel = computed<string>(() => (props.label ? t(props.label).toLowerCase() : inputName.value));
const { errorMessage, handleChange, meta, value } = useField<string>(inputName, validationRules, {
initialValue: props.modelValue || undefined,
label: displayLabel,
});
const status = computed<InputStatus | undefined>(() => {
if (!meta.dirty && !meta.touched) {
return undefined;
}
return meta.valid ? "valid" : "invalid";
});
const validationListeners = computed<ValidationListeners>(() => ({
blur: handleChange,
change: handleChange,
input: errorMessage.value ? handleChange : (e: unknown) => handleChange(e, false),
}));
function focus(): void {
inputRef.value?.focus();
}
defineExpose({ focus });
</script>

<template>
<TarInput
:described-by="describedBy"
:disabled="disabled"
:floating="floating"
:id="id"
:label="label ? t(label) : undefined"
:max="inputMax"
:min="inputMin"
:model-value="value"
:name="name"
:placeholder="placeholder ? t(placeholder) : undefined"
:plaintext="plaintext"
:readonly="readonly"
ref="inputRef"
:required="required ? 'label' : undefined"
:size="size"
:status="status"
:step="step"
:type="type"
v-on="validationListeners"
>
<template #after>
<div v-if="errorMessage" class="invalid-feedback" :id="describedBy">{{ errorMessage }}</div>
<slot name="after"></slot>
</template>
</TarInput>
</template>
23 changes: 23 additions & 0 deletions frontend/src/types/validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export type ValidationListeners = {
blur: (e: unknown, shouldValidate?: boolean) => void;
change: (e: unknown, shouldValidate?: boolean) => void;
input: (e: unknown, shouldValidate?: boolean) => void;
};

export type ValidationRules = {
confirmed?: string[];
email?: boolean;
max_length?: number;
max_value?: number;
min_length?: number;
min_value?: number;
regex?: string;
require_digit?: boolean;
require_lowercase?: boolean;
require_non_alphanumeric?: boolean;
require_uppercase?: boolean;
required?: boolean;
unique_chars?: number;
url?: boolean;
username?: string;
};
30 changes: 30 additions & 0 deletions frontend/src/views/account/ProfileView.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,35 @@
<script setup lang="ts">
import { TarButton } from "logitar-vue3-ui";
import { inject, ref } from "vue";
import { useForm } from "vee-validate";
import { useI18n } from "vue-i18n";
import AppInput from "@/components/shared/AppInput.vue";
import { handleErrorKey } from "@/inject/App";
const handleError = inject(handleErrorKey) as (e: unknown) => void;
const { t } = useI18n();
const emailAddress = ref<string>("");
const password = ref<string>("");
const { handleSubmit, isSubmitting } = useForm();
const onSubmit = handleSubmit(async () => {
try {
alert(`Hello ${emailAddress.value}!`);
} catch (e: unknown) {
handleError(e);
}
});
</script>

<template>
<main class="container">
<h1>Profile</h1>
<form @submit.prevent="onSubmit">
<AppInput floating label="users.email.address" placeholder="users.email.address" required type="email" v-model="emailAddress" />
<AppInput floating label="users.password" placeholder="users.password" required type="password" v-model="password" />
<TarButton :disabled="isSubmitting" icon="fas fa-user" :loading="isSubmitting" :status="t('loading')" :text="t('actions.save')" type="submit" />
</form>
</main>
</template>

0 comments on commit b3187f6

Please sign in to comment.