Skip to content

Commit

Permalink
Provided validation and helper slot for FwbSelect #242 (#254)
Browse files Browse the repository at this point in the history
* feat(fwb-select): Add validation and helper slot

* docs(docs-select): Add validation and helper slot examples
  • Loading branch information
imanmalekian31 authored Dec 5, 2023
1 parent 32d9c4a commit f530e24
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 53 deletions.
74 changes: 74 additions & 0 deletions docs/components/select.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<script setup>
import FwbSelectExample from './select/examples/FwbSelectExample.vue'
import FwbSelectExampleDisabled from './select/examples/FwbSelectExampleDisabled.vue'
import FwbSelectExampleHelper from './select/examples/FwbSelectExampleHelper.vue'
import FwbSelectExampleSize from './select/examples/FwbSelectExampleSize.vue'
import FwbSelectExampleUnderlined from './select/examples/FwbSelectExampleUnderlined.vue'
import FwbSelectExampleValidation from './select/examples/FwbSelectExampleValidation.vue'
</script>

# Vue Select - Flowbite
Expand Down Expand Up @@ -132,3 +134,75 @@ const countries = [
]
</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 changes: 31 additions & 0 deletions docs/components/select/examples/FwbSelectExampleHelper.vue
Original file line number Diff line number Diff line change
@@ -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>
33 changes: 33 additions & 0 deletions docs/components/select/examples/FwbSelectExampleValidation.vue
Original file line number Diff line number Diff line change
@@ -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>
105 changes: 52 additions & 53 deletions src/components/FwbSelect/FwbSelect.vue
Original file line number Diff line number Diff line change
@@ -1,40 +1,55 @@
<template>
<label>
<span
v-if="label"
:class="labelClasses"
>
{{ label }}
</span>
<select
v-model="model"
:disabled="disabled"
:class="selectClasses"
>
<option
disabled
selected
value=""
<div>
<label>
<span
v-if="label"
:class="labelClasses"
>
{{ placeholder }}
</option>
<option
v-for="(option, index) in options"
:key="index"
:value="option.value"
{{ label }}
</span>
<select
v-model="model"
:disabled="disabled"
:class="selectClasses"
>
{{ option.name }}
</option>
</select>
</label>
<option
disabled
selected
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>

<script lang="ts" setup>
import type { InputSize } from './../FwbInput/types'
import type { OptionsType } from './types'
import { computed } from 'vue'
import { computed, toRefs } from 'vue'
import { useVModel } from '@vueuse/core'
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 {
modelValue?: string;
Expand All @@ -44,6 +59,7 @@ interface InputProps {
disabled?: boolean;
underline?: boolean;
size?: InputSize;
validationStatus?: ValidationStatus
}
const props = withDefaults(defineProps<InputProps>(), {
modelValue: '',
Expand All @@ -53,34 +69,17 @@ const props = withDefaults(defineProps<InputProps>(), {
disabled: false,
underline: false,
size: 'md',
validationStatus: undefined,
})
const emit = defineEmits(['update:modelValue'])
const model = useVModel(props, 'modelValue', emit)
// LABEL
const defaultLabelClasses = 'block mb-2 text-sm font-medium text-gray-900 dark:text-white'
const { selectClasses, labelClasses } = useSelectClasses(toRefs(props))
// SELECT
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 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
})
const validationWrapperClasses = computed(() => twMerge(
'mt-2 text-sm',
props.validationStatus === validationStatusMap.Success ? 'text-green-600 dark:text-green-500' : '',
props.validationStatus === validationStatusMap.Error ? 'text-red-600 dark:text-red-500' : '',
))
</script>
76 changes: 76 additions & 0 deletions src/components/FwbSelect/composables/useSelectClasses.ts
Original file line number Diff line number Diff line change
@@ -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,
}
}
9 changes: 9 additions & 0 deletions src/components/FwbSelect/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
export type InputSize = 'sm' | 'md' | 'lg'

export type OptionsType = {
name: string,
value: string,
}

export const validationStatusMap = {
Success: 'success',
Error: 'error',
} as const

export type ValidationStatus = typeof validationStatusMap[keyof typeof validationStatusMap]

0 comments on commit f530e24

Please sign in to comment.