* feat(fwb-select): Add validation and helper slot * docs(docs-select): Add validation and helper slot examples
This commit is contained in:
@@ -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>
|
||||||
|
```
|
||||||
|
|||||||
31
docs/components/select/examples/FwbSelectExampleHelper.vue
Normal file
31
docs/components/select/examples/FwbSelectExampleHelper.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user