rework inputs, again...

hopefully this is the last time, but code wise its nicer now and structure wise we can now have elements "inside" of the input for counter, loading, etc
This commit is contained in:
MiniDigger | Martin 2022-04-16 10:07:51 +02:00
parent 58a2eb77c3
commit 7af707538f
9 changed files with 125 additions and 86 deletions

View File

@ -2,6 +2,7 @@
import { computed } from "vue";
import { useValidation } from "~/composables/useValidationHelpers";
import { ValidationRule } from "@vuelidate/core";
import InputWrapper from "~/components/ui/InputWrapper.vue";
const emit = defineEmits<{
(e: "update:modelValue", date: string): void;
@ -14,6 +15,7 @@ const props = defineProps<{
modelValue: string;
label?: string;
disabled?: boolean;
loading?: boolean;
errorMessages?: string[];
rules?: ValidationRule<string | undefined>[];
}>();
@ -22,6 +24,8 @@ const { v, errors, hasError } = useValidation(props.label, props.rules, date, pr
</script>
<template>
<!-- todo make fancy -->
<input v-model="date" type="date" v-bind="$attrs" :disabled="disabled" />
<InputWrapper v-slot="slotProps" :errors="errors" :has-error="hasError" :loading="loading || v.$pending" :label="label" :value="date">
<!-- todo make fancy -->
<input v-model="date" type="date" v-bind="$attrs" :disabled="disabled" :class="slotProps.class" />
</InputWrapper>
</template>

View File

@ -2,6 +2,7 @@
import { computed } from "vue";
import { useValidation } from "~/composables/useValidationHelpers";
import { ValidationRule } from "@vuelidate/core";
import InputWrapper from "~/components/ui/InputWrapper.vue";
const emit = defineEmits<{
(e: "update:modelValue", file: string): void;
@ -15,6 +16,7 @@ const props = defineProps<{
label?: string;
disabled?: boolean;
showSize?: boolean;
loading?: boolean;
errorMessages?: string[];
rules?: ValidationRule<string | undefined>[];
}>();
@ -23,6 +25,8 @@ const { v, errors, hasError } = useValidation(props.label, props.rules, file, pr
</script>
<template>
<!-- todo make fancy, implement functionality -->
<input type="file" v-bind="$attrs" :disabled="disabled" />
<InputWrapper v-slot="slotProps" :errors="errors" :has-error="hasError" :loading="loading || v.$pending" :label="label" :value="file">
<!-- todo make fancy, implement functionality -->
<input type="file" v-bind="$attrs" :disabled="disabled" :class="slotProps.class" />
</InputWrapper>
</template>

View File

@ -1,9 +1,8 @@
<script setup lang="ts">
import { computed } from "vue";
import { FloatingLabel, inputClasses } from "~/composables/useInputHelper";
import ErrorTooltip from "~/components/design/ErrorTooltip.vue";
import { useValidation } from "~/composables/useValidationHelpers";
import { ValidationRule } from "@vuelidate/core";
import InputWrapper from "~/components/ui/InputWrapper.vue";
const emit = defineEmits<{
(e: "update:modelValue", value: object | string | boolean | number | null | undefined): void;
@ -26,6 +25,7 @@ const props = withDefaults(
itemText?: string;
disabled?: boolean;
label?: string;
loading?: boolean;
errorMessages?: string[];
rules?: ValidationRule<string | undefined>[];
}>(),
@ -34,6 +34,7 @@ const props = withDefaults(
itemValue: "value",
itemText: "text",
label: "",
loading: false,
errorMessages: () => [],
rules: () => [],
}
@ -43,14 +44,11 @@ const { v, errors, hasError } = useValidation(props.label, props.rules, internal
</script>
<template>
<ErrorTooltip :error-messages="errors" class="w-full">
<label class="relative flex" :class="{ filled: internalVal, error: hasError }">
<select v-model="internalVal" :disabled="disabled" :class="inputClasses" @blur="v.$touch()">
<option v-for="val in values" :key="val[itemValue] || val" :value="val[itemValue] || val" class="dark:bg-[#191e28]">
{{ val[itemText] || val }}
</option>
</select>
<floating-label :label="label" />
</label>
</ErrorTooltip>
<InputWrapper v-slot="slotProps" :errors="errors" :has-error="hasError" :loading="loading || v.$pending" :label="label" :value="internalVal">
<select v-model="internalVal" :disabled="disabled" :class="slotProps.class" @blur="v.$touch()">
<option v-for="val in values" :key="val[itemValue] || val" :value="val[itemValue] || val" class="dark:bg-[#191e28]">
{{ val[itemText] || val }}
</option>
</select>
</InputWrapper>
</template>

View File

@ -1,9 +1,8 @@
<script lang="ts" setup>
import { computed, ref } from "vue";
import { FloatingLabel, inputClasses } from "~/composables/useInputHelper";
import ErrorTooltip from "~/components/design/ErrorTooltip.vue";
import { useValidation } from "~/composables/useValidationHelpers";
import { ValidationRule } from "@vuelidate/core";
import InputWrapper from "~/components/ui/InputWrapper.vue";
const tag = ref<string>("");
const emit = defineEmits<{
@ -18,6 +17,7 @@ const props = defineProps<{
label?: string;
counter?: boolean;
maxlength?: number;
loading?: boolean;
errorMessages?: string[];
rules?: ValidationRule<string | undefined>[];
}>();
@ -40,18 +40,27 @@ function add() {
</script>
<template>
<ErrorTooltip :error-messages="errors" class="w-full">
<div class="relative flex items-center pointer-events-none text-left" :class="{ filled: (modelValue && modelValue.length) || tag, error: hasError }">
<div :class="inputClasses" class="flex">
<span v-for="t in tags" :key="t" class="bg-primary-light-400 rounded-4xl px-1 py-1 mx-1 h-30px inline-flex items-center" dark="text-black">
{{ t }}
<button class="text-gray-400 ml-1 inline-flex pointer-events-auto" hover="text-gray-500" @click="remove(t)"><icon-mdi-close-circle /></button>
</span>
<input v-model="tag" type="text" class="pointer-events-auto outline-none bg-transparent flex-grow" @keydown.enter="add" @blur="v.$touch()" />
<floating-label :label="label" />
</div>
<span v-if="counter && maxlength">{{ tags?.length || 0 }}/{{ maxlength }}</span>
<span v-else-if="counter">{{ tags?.length || 0 }}</span>
</div>
</ErrorTooltip>
<InputWrapper
v-slot="slotProps"
:errors="errors"
:has-error="hasError"
:counter="counter"
:maxlength="maxlength"
:loading="loading || v.$pending"
:label="label"
:value="tags"
>
<span v-for="t in tags" :key="t" class="bg-primary-light-400 rounded-4xl px-1 py-1 mx-1 h-30px inline-flex items-center" dark="text-black">
{{ t }}
<button class="text-gray-400 ml-1 inline-flex pointer-events-auto" hover="text-gray-500" @click="remove(t)"><icon-mdi-close-circle /></button>
</span>
<input
v-model="tag"
type="text"
class="pointer-events-auto outline-none bg-transparent flex-grow"
:class="slotProps.class"
@keydown.enter="add"
@blur="v.$touch()"
/>
</InputWrapper>
</template>

View File

@ -1,9 +1,8 @@
<script lang="ts" setup>
import { computed } from "vue";
import { FloatingLabel, inputClasses } from "~/composables/useInputHelper";
import ErrorTooltip from "~/components/design/ErrorTooltip.vue";
import { ValidationRule } from "@vuelidate/core";
import { useValidation } from "~/composables/useValidationHelpers";
import InputWrapper from "~/components/ui/InputWrapper.vue";
const emit = defineEmits<{
(e: "update:modelValue", value?: string): void;
@ -17,6 +16,7 @@ const props = defineProps<{
label?: string;
counter?: boolean;
maxlength?: number;
loading?: boolean;
errorMessages?: string[];
rules?: ValidationRule<string | undefined>[];
}>();
@ -25,11 +25,16 @@ const { v, errors, hasError } = useValidation(props.label, props.rules, value, p
</script>
<template>
<ErrorTooltip :error-messages="errors" class="w-full">
<label class="relative flex" :class="{ filled: modelValue, error: hasError }">
<input v-model="value" type="text" :class="inputClasses" v-bind="$attrs" :maxlength="maxlength" @blur="v.$touch()" />
<span v-if="counter && maxlength" class="inline-flex items-center ml-2">{{ value?.length || 0 }}/{{ maxlength }}</span>
<span v-else-if="counter">{{ value?.length || 0 }}</span>
<floating-label :label="label" /> </label
></ErrorTooltip>
<InputWrapper
v-slot="slotProps"
:errors="errors"
:has-error="hasError"
:counter="counter"
:maxlength="maxlength"
:loading="loading || v.$pending"
:label="label"
:value="value"
>
<input v-model="value" type="text" v-bind="$attrs" :maxlength="maxlength" :class="slotProps.class" @blur="v.$touch()" />
</InputWrapper>
</template>

View File

@ -1,9 +1,8 @@
<script lang="ts" setup>
import { computed } from "vue";
import { FloatingLabel, inputClasses } from "~/composables/useInputHelper";
import ErrorTooltip from "~/components/design/ErrorTooltip.vue";
import { useValidation } from "~/composables/useValidationHelpers";
import { ValidationRule } from "@vuelidate/core";
import InputWrapper from "~/components/ui/InputWrapper.vue";
const emit = defineEmits<{
(e: "update:modelValue", value: string): void;
@ -17,6 +16,7 @@ const props = defineProps<{
label?: string;
counter?: boolean;
maxlength?: number;
loading?: boolean;
errorMessages?: string[];
rules?: ValidationRule<string | undefined>[];
}>();
@ -25,14 +25,16 @@ const { v, errors, hasError } = useValidation(props.label, props.rules, value, p
</script>
<template>
<ErrorTooltip :error-messages="errors" class="w-full">
<label class="relative flex" :class="{ filled: modelValue, error: hasError }">
<textarea v-model="value" :class="inputClasses" v-bind="$attrs" :maxlength="maxlength" @blur="v.$touch()" />
<floating-label :label="label" />
</label>
<div v-if="counter" class="mt-1 mb-2">
<span v-if="maxlength">{{ value?.length || 0 }}/{{ maxlength }}</span>
<span v-else>{{ value?.length || 0 }}</span>
</div>
</ErrorTooltip>
<InputWrapper
v-slot="slotProps"
:errors="errors"
:has-error="hasError"
:counter="counter"
:maxlength="maxlength"
:loading="loading || v.$pending"
:label="label"
:value="value"
>
<textarea v-model="value" v-bind="$attrs" :maxlength="maxlength" :class="slotProps.class" @blur="v.$touch()" />
</InputWrapper>
</template>

View File

@ -0,0 +1,47 @@
<script lang="ts" setup>
import ErrorTooltip from "~/components/design/ErrorTooltip.vue";
const props = defineProps<{
errors?: string[];
hasError: boolean;
label?: string;
counter?: boolean;
maxlength?: number;
loading?: boolean;
value: unknown;
}>();
</script>
<template>
<ErrorTooltip :error-messages="errors" class="w-full" :class="{ filled: value, error: hasError }">
<label
:class="[
'relative flex w-full outline-none p-2 border-bottom-1px rounded',
'bg-primary-light-200 border-gray-400 dark:(bg-primary-300/8 border-gray-500)',
'focus:(bg-primary-200/5 dark:bg-primary-200/10 border-primary-400)',
'error:(border-red-400) disabled:(bg-black-15 text-black-50)',
'transition duration-200 ease',
]"
>
<slot class="outline-none flex-grow bg-transparent"></slot>
<span class="flex pl-2">
<span v-if="counter && maxlength" class="inline-flex items-center ml-2">{{ value?.length || 0 }}/{{ maxlength }}</span>
<span v-else-if="counter">{{ value?.length || 0 }}</span>
<!-- todo proper loading indicator -->
<span v-if="loading">Loading...</span>
</span>
<span
v-if="label"
:class="[
'absolute origin-top-left left-2 italic',
'input-focused:(transform scale-62 opacity-100 not-italic) filled:(transform scale-62 text-black-50 not-italic)',
'opacity-80 error:(!text-red-400) input-focused:(text-primary-300)',
'top-10px input-focused:(top-0) filled:(top-0)',
'transition duration-250 ease',
]"
>
{{ label }}
</span>
</label>
</ErrorTooltip>
</template>

View File

@ -1,31 +0,0 @@
import { defineComponent, h } from "vue";
export const inputClasses =
"w-full outline-none p-2 border-bottom-1px rounded " +
"bg-primary-light-200 border-gray-400 dark:(bg-primary-300/8 border-gray-500) " +
"focus:(bg-primary-200/5 dark:bg-primary-200/10 border-primary-400) " +
"error:(border-red-400) " +
"disabled:(bg-black-15 text-black-50) " +
"transition duration-200 ease";
export const labelClasses = [
"absolute origin-top-left left-2 italic",
"input-focused:(transform scale-62 opacity-100 not-italic) filled:(transform scale-62 text-black-50 not-italic)",
"opacity-80 error:(!text-red-400) input-focused:(text-primary-300)",
"top-10px input-focused:(top-0) filled:(top-0)",
"transition duration-250 ease",
];
export const FloatingLabel = defineComponent({
name: "FloatingLabel",
props: {
label: {
type: String,
required: false,
default: "",
},
},
render() {
return this.label ? h("p", { class: labelClasses }, this.label) : [];
},
});

View File

@ -4,6 +4,7 @@ import * as validators from "@vuelidate/validators";
import { createI18nMessage, helpers, ValidatorWrapper } from "@vuelidate/validators";
import { I18n } from "~/i18n";
import { useInternalApi } from "~/composables/useApi";
import { AxiosError } from "axios";
export function isErrorObject(errorObject: string | ErrorObject): errorObject is ErrorObject {
return (<ErrorObject>errorObject).$message !== undefined;
@ -32,7 +33,7 @@ export function useValidation<T>(
}
return e;
});
const hasError = computed(() => (errorMessages && errorMessages.length) || v.value.$error);
const hasError = computed(() => (errorMessages && errorMessages.length > 0) || v.value.$error);
return { v, errors, hasError };
}
@ -82,7 +83,7 @@ export const validName = withOverrideMessage(
value: value,
});
return { $valid: true };
} catch (e) {
} catch (e: any) {
return !e.response?.data.isHangarApiException ? { $valid: false } : { $valid: false, $message: e.response?.data.message };
}
})