Provided validation and helper slot for FwbSelect #242 (#254)

* feat(fwb-select): Add validation and helper slot

* docs(docs-select): Add validation and helper slot examples
This commit is contained in:
imanmalekian31
2023-12-05 19:32:36 +03:30
committed by GitHub
parent 32d9c4a5bc
commit f530e24a41
6 changed files with 275 additions and 53 deletions

View File

@@ -1,8 +1,10 @@
<script setup> <script setup>
import FwbSelectExample from './select/examples/FwbSelectExample.vue' import FwbSelectExample from './select/examples/FwbSelectExample.vue'
import FwbSelectExampleDisabled from './select/examples/FwbSelectExampleDisabled.vue' import FwbSelectExampleDisabled from './select/examples/FwbSelectExampleDisabled.vue'
import FwbSelectExampleHelper from './select/examples/FwbSelectExampleHelper.vue'
import FwbSelectExampleSize from './select/examples/FwbSelectExampleSize.vue' import FwbSelectExampleSize from './select/examples/FwbSelectExampleSize.vue'
import FwbSelectExampleUnderlined from './select/examples/FwbSelectExampleUnderlined.vue' import FwbSelectExampleUnderlined from './select/examples/FwbSelectExampleUnderlined.vue'
import FwbSelectExampleValidation from './select/examples/FwbSelectExampleValidation.vue'
</script> </script>
# Vue Select - Flowbite # Vue Select - Flowbite
@@ -132,3 +134,75 @@ const countries = [
] ]
</script> </script>
``` ```
## Slot - Helper
<fwb-select-example-helper />
```vue
<template>
<fwb-select
v-model="selected"
:options="countries"
label="Select a country"
>
<template #helper>
We'll never share your details. Read our
<fwb-a href="#" color="text-blue-600 dark:text-blue-500">
Privacy Policy
</fwb-a>.
</template>
</fwb-input>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { FwbA, FwbSelect } from 'flowbite-vue'
const selected = ref('')
const countries = [
{ value: 'us', name: 'United States' },
{ value: 'ca', name: 'Canada' },
{ value: 'fr', name: 'France' },
]
</script>
```
## Slot - Validation
- Set validation status via `validationStatus` prop, which accepts `'success'` or `'error'`.
- Add validation message via `validationMessage` slot.
<fwb-select-example-validation />
```vue
<template>
<fwb-select
v-model="selected"
:options="countries"
label="Select a country"
validation-status="success"
/>
<hr class="mt-4 border-0">
<fwb-select
v-model="selected"
:options="countries"
label="Select a country"
validation-status="error"
>
<template #validationMessage>
Please select a country
</template>
</fwb-select>
</template>
<script setup>
import { ref } from 'vue'
import { FwbSelect } from 'flowbite-vue'
const selected = ref('')
const countries = [
{ value: 'us', name: 'United States' },
{ value: 'ca', name: 'Canada' },
{ value: 'fr', name: 'France' },
]
</script>
```

View File

@@ -0,0 +1,31 @@
<template>
<div class="vp-raw">
<fwb-select
v-model="selected"
:options="countries"
label="Select a country"
>
<template #helper>
We'll never share your details. Read our
<fwb-a
href="#"
color="text-blue-600 dark:text-blue-500"
>
Privacy Policy
</fwb-a>.
</template>
</fwb-select>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { FwbA, FwbSelect } from '../../../../src/index'
const selected = ref('')
const countries = [
{ value: 'us', name: 'United States' },
{ value: 'ca', name: 'Canada' },
{ value: 'fr', name: 'France' },
]
</script>

View File

@@ -0,0 +1,33 @@
<template>
<div class="vp-raw">
<fwb-select
v-model="selected"
:options="countries"
label="Select a country"
validation-status="success"
/>
<hr class="mt-4 border-0">
<fwb-select
v-model="selected"
:options="countries"
label="Select a country"
validation-status="error"
>
<template #validationMessage>
Please select a country
</template>
</fwb-select>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { FwbSelect } from '../../../../src/index'
const selected = ref('')
const countries = [
{ value: 'us', name: 'United States' },
{ value: 'ca', name: 'Canada' },
{ value: 'fr', name: 'France' },
]
</script>

View File

@@ -1,40 +1,55 @@
<template> <template>
<label> <div>
<span <label>
v-if="label" <span
:class="labelClasses" v-if="label"
> :class="labelClasses"
{{ label }}
</span>
<select
v-model="model"
:disabled="disabled"
:class="selectClasses"
>
<option
disabled
selected
value=""
> >
{{ placeholder }} {{ label }}
</option> </span>
<option <select
v-for="(option, index) in options" v-model="model"
:key="index" :disabled="disabled"
:value="option.value" :class="selectClasses"
> >
{{ option.name }} <option
</option> disabled
</select> selected
</label> value=""
>
{{ placeholder }}
</option>
<option
v-for="(option, index) in options"
:key="index"
:value="option.value"
>
{{ option.name }}
</option>
</select>
</label>
<p
v-if="$slots.validationMessage"
:class="validationWrapperClasses"
>
<slot name="validationMessage" />
</p>
<p
v-if="$slots.helper"
class="mt-2 text-sm text-gray-500 dark:text-gray-400"
>
<slot name="helper" />
</p>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type { InputSize } from './../FwbInput/types' import { computed, toRefs } from 'vue'
import type { OptionsType } from './types'
import { computed } from 'vue'
import { useVModel } from '@vueuse/core' import { useVModel } from '@vueuse/core'
import { twMerge } from 'tailwind-merge' import { twMerge } from 'tailwind-merge'
import { useSelectClasses } from './composables/useSelectClasses'
import type { InputSize } from './../FwbInput/types'
import { type OptionsType, type ValidationStatus, validationStatusMap } from './types'
interface InputProps { interface InputProps {
modelValue?: string; modelValue?: string;
@@ -44,6 +59,7 @@ interface InputProps {
disabled?: boolean; disabled?: boolean;
underline?: boolean; underline?: boolean;
size?: InputSize; size?: InputSize;
validationStatus?: ValidationStatus
} }
const props = withDefaults(defineProps<InputProps>(), { const props = withDefaults(defineProps<InputProps>(), {
modelValue: '', modelValue: '',
@@ -53,34 +69,17 @@ const props = withDefaults(defineProps<InputProps>(), {
disabled: false, disabled: false,
underline: false, underline: false,
size: 'md', size: 'md',
validationStatus: undefined,
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const model = useVModel(props, 'modelValue', emit) const model = useVModel(props, 'modelValue', emit)
// LABEL const { selectClasses, labelClasses } = useSelectClasses(toRefs(props))
const defaultLabelClasses = 'block mb-2 text-sm font-medium text-gray-900 dark:text-white'
// SELECT const validationWrapperClasses = computed(() => twMerge(
const defaultSelectClasses = 'w-full text-gray-900 bg-gray-50 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500' 'mt-2 text-sm',
const disabledSelectClasses = 'cursor-not-allowed bg-gray-100' props.validationStatus === validationStatusMap.Success ? 'text-green-600 dark:text-green-500' : '',
const underlineSelectClasses = 'bg-transparent dark:bg-transparent border-b-2 border-gray-200 appearance-none dark:border-gray-700 focus:outline-none focus:ring-0 focus:border-gray-200 peer' props.validationStatus === validationStatusMap.Error ? 'text-red-600 dark:text-red-500' : '',
const selectSizeClasses: Record<InputSize, string> = { ))
lg: 'p-4',
md: 'p-2.5 text-sm',
sm: 'p-2 text-sm',
}
const selectClasses = computed(() => {
return twMerge(
defaultSelectClasses,
selectSizeClasses[props.size],
props.disabled && disabledSelectClasses,
props.underline ? underlineSelectClasses : 'border border-gray-300 rounded-lg',
)
})
const labelClasses = computed(() => {
return defaultLabelClasses
})
</script> </script>

View File

@@ -0,0 +1,76 @@
import { computed, type Ref } from 'vue'
import { twMerge } from 'tailwind-merge'
import {
type InputSize,
type ValidationStatus,
validationStatusMap,
} from '../types'
// LABEL
const baseLabelClasses = 'block mb-2 text-sm font-medium'
// INPUT
const defaultSelectClasses = 'w-full text-gray-900 bg-gray-50 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500'
const disabledSelectClasses = 'cursor-not-allowed bg-gray-100'
const underlineSelectClasses = 'bg-transparent dark:bg-transparent border-b-2 border-gray-200 appearance-none dark:border-gray-700 focus:outline-none focus:ring-0 focus:border-gray-200 peer'
const selectSizeClasses: Record<InputSize, string> = {
lg: 'p-4',
md: 'p-2.5 text-sm',
sm: 'p-2 text-sm',
}
const successInputClasses = 'bg-green-50 border-green-500 dark:border-green-500 text-green-900 dark:text-green-400 placeholder-green-700 dark:placeholder-green-500 focus:ring-green-500 focus:border-green-500'
const errorInputClasses = 'bg-red-50 border-red-500 text-red-900 placeholder-red-700 focus:ring-red-500 focus:border-red-500 dark:text-red-500 dark:placeholder-red-500 dark:border-red-500'
export type UseSelectClassesProps = {
size: Ref<InputSize>,
disabled: Ref<boolean>
underline: Ref<boolean>,
validationStatus: Ref<ValidationStatus | undefined>
}
export function useSelectClasses (props: UseSelectClassesProps): {
selectClasses: Ref<string>
labelClasses: Ref<string>
} {
const selectClasses = computed(() => {
const vs = props.validationStatus.value
const classByStatus = vs === validationStatusMap.Success
? successInputClasses
: vs === validationStatusMap.Error
? errorInputClasses
: ''
const underlineByStatus = vs === validationStatusMap.Success
? 'focus:border-green-500'
: vs === validationStatusMap.Error
? 'focus:border-red-500'
: ''
return twMerge(
defaultSelectClasses,
classByStatus,
selectSizeClasses[props.size.value],
props.disabled.value && disabledSelectClasses,
props.underline.value ? underlineSelectClasses : 'border border-gray-300 rounded-lg',
props.underline.value && underlineByStatus,
)
})
const labelClasses = computed(() => {
const vs = props.validationStatus.value
const classByStatus = vs === validationStatusMap.Success
? 'text-green-700 dark:text-green-500'
: vs === validationStatusMap.Error
? 'text-red-700 dark:text-red-500'
: 'text-gray-900 dark:text-white'
return twMerge(baseLabelClasses, classByStatus)
})
return {
selectClasses,
labelClasses,
}
}

View File

@@ -1,4 +1,13 @@
export type InputSize = 'sm' | 'md' | 'lg'
export type OptionsType = { export type OptionsType = {
name: string, name: string,
value: string, value: string,
} }
export const validationStatusMap = {
Success: 'success',
Error: 'error',
} as const
export type ValidationStatus = typeof validationStatusMap[keyof typeof validationStatusMap]