ENH: Phone Input Component (#189)

* #170-ENH: Created custom dropdown phone input

* #170-ENH: Added phone_number rules

* #170-ENH: Added phone_number rules

* #170-ENH: Added separate Rule for phone number input, starting 0 phone number is ignored, added regex to ignore non digit phone input

* #170-ENH: Removed global registration of CountryFlag

* #170-ENH: Using VSelect component for country selection, added prop for dropdown styling

* #170-ENH: Updated phone number rule

* #170-ENH: Added margins to country selector

---------

Co-authored-by: Sutirtha <sdas@republicfinance.com>
Co-authored-by: Julien Nahum <julien@nahum.net>
This commit is contained in:
Sutirtha Bharati Das 2023-09-12 13:43:10 +05:30 committed by GitHub
parent f775ab84c7
commit a53677d2ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 1377 additions and 7 deletions

View File

@ -11,6 +11,7 @@ use Illuminate\Support\Str;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Rules\ValidHCaptcha; use App\Rules\ValidHCaptcha;
use App\Rules\ValidPhoneInputRule;
class AnswerFormRequest extends FormRequest class AnswerFormRequest extends FormRequest
{ {
@ -150,7 +151,6 @@ class AnswerFormRequest extends FormRequest
{ {
switch ($property['type']) { switch ($property['type']) {
case 'text': case 'text':
case 'phone_number':
case 'signature': case 'signature':
return ['string']; return ['string'];
case 'number': case 'number':
@ -189,6 +189,8 @@ class AnswerFormRequest extends FormRequest
return ['array', 'min:2']; return ['array', 'min:2'];
} }
return $this->getRulesForDate($property); return $this->getRulesForDate($property);
case 'phone_number':
return [new ValidPhoneInputRule];
default: default:
return []; return [];
} }

View File

@ -0,0 +1,29 @@
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Support\Str;
class ValidPhoneInputRule implements Rule
{
public function passes($attribute, $value)
{
if (!is_string($value)) {
return false;
}
if (!Str::startsWith($value, '+')) {
return false;
}
$parts = explode(' ', $value);
if (count($parts) < 2) {
return false;
}
return strlen($parts[1]) >= 5;
}
public function message()
{
return 'The :attribute must be a string that starts with a "+" character and must be at least 5 digits long.';
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,101 @@
<template>
<div :class="wrapperClass" :style="inputStyle">
<slot name="label">
<label v-if="label" :for="id ? id : name"
:class="[theme.default.label, { 'uppercase text-xs': uppercaseLabels, 'text-sm': !uppercaseLabels }]">
{{ label }}
<span v-if="required" class="text-red-500 required-dot">*</span>
</label>
</slot>
<div v-if="help && helpPosition == 'above_input'" class="flex mb-1">
<small :class="theme.default.help" class="grow">
<slot name="help"><span class="field-help" v-html="help" /></slot>
</small>
</div>
<div :id="id ? id : name" :disabled="disabled" :name="name" :style="inputStyle" class="flex items-center">
<v-select class="w-1/4 mt-1" :data="countries" :value="selectedCountryCode" :inner-style="{ width: '474px' }"
:searchable="true" :search-keys="['name']" :option-key="'code'" :color="'#3B82F6'"
:placeholder="'Select a country'" :uppercase-labels="true" :theme="theme" @input="onCountryChange">
<template #option="props">
<div class="flex items-center space-x-2">
<country-flag :country="props.option.code" />
<span>{{ props.option.name }}</span>
<span>{{ props.option.code }}</span>
<span>{{ props.option.dial_code }}</span>
</div>
</template>
<template #selected="props">
<div class="flex items-center space-x-2 justify-start">
<country-flag :country="props.option.code" :style="{
'margin-top': '-0.7em',
'margin-left': '-0.9em',
'margin-right': '-0.6em',
'margin-bottom': '-0.7em'
}" />
<span>{{ props.option.dial_code }}</span>
</div>
</template>
</v-select>
<input v-model="inputVal" type="text" class="inline-flex-grow ml-5"
:class="[theme.default.input, { '!ring-red-500 !ring-2': hasValidation && form.errors.has(name), '!cursor-not-allowed !bg-gray-200': disabled }]"
:placeholder="placeholder" :style="inputStyle" @input="onInput">
</div>
</div>
</template>
<script>
import { directive as onClickaway } from 'vue-clickaway'
import inputMixin from '~/mixins/forms/input.js'
import countryCodes from '../../../data/country_codes.json'
import CountryFlag from 'vue-country-flag'
import VSelect from './components/VSelect.vue'
export default {
phone: 'PhoneInput',
components: {
CountryFlag, VSelect
},
directives: {
onClickaway: onClickaway
},
mixins: [inputMixin],
data() {
return {
selectedCountryCode: countryCodes[234],
countries: countryCodes,
isOpen: false,
inputVal: ''
}
},
watch: {
inputVal(newVal, oldVal) {
if (newVal.startsWith('0')) {
newVal = newVal.replace(/^0+/, '')
}
this.compVal = this.selectedCountryCode.dial_code + ' ' + newVal
},
selectedCountryCode(newVal, oldVal) {
if (this.compVal) {
this.compVal = this.compVal.replace(oldVal.dial_code, newVal.dial_code)
}
}
},
methods: {
onCountryChange(country) {
this.selectedCountryCode = country
this.closeDropdown()
},
closeDropdown() {
this.isOpen = false
},
onInput(event) {
const input = event.target.value
const digitsOnly = input.replace(/[^0-9]/g, '')
this.inputVal = digitsOnly
},
}
}
</script>
<style scoped></style>

View File

@ -43,6 +43,7 @@
<!-- Select popover, show/hide based on select state. --> <!-- Select popover, show/hide based on select state. -->
<div v-show="isOpen" :dusk="dusk+'_dropdown' " <div v-show="isOpen" :dusk="dusk+'_dropdown' "
class="absolute mt-1 w-full rounded-md bg-white dark:bg-notion-dark-light shadow-lg z-10" class="absolute mt-1 w-full rounded-md bg-white dark:bg-notion-dark-light shadow-lg z-10"
:style="innerStyle"
> >
<ul tabindex="-1" role="listbox" aria-labelled by="listbox-label" aria-activedescendant="listbox-item-3" <ul tabindex="-1" role="listbox" aria-labelled by="listbox-label" aria-activedescendant="listbox-item-3"
class="rounded-md text-base leading-6 shadow-xs overflow-auto focus:outline-none sm:text-sm sm:leading-5 relative" class="rounded-md text-base leading-6 shadow-xs overflow-auto focus:outline-none sm:text-sm sm:leading-5 relative"
@ -99,6 +100,7 @@ export default {
props: { props: {
data: Array, data: Array,
value: { default: null }, value: { default: null },
innerStyle: { type: Object, default: null },
label: { type: String, default: null }, label: { type: String, default: null },
dusk: { type: String, default: null }, dusk: { type: String, default: null },
loading: { type: Boolean, default: false }, loading: { type: Boolean, default: false },

View File

@ -41,3 +41,4 @@ import ToggleSwitchInput from './ToggleSwitchInput.vue'
Vue.component('SignatureInput', () => import('./SignatureInput.vue')) Vue.component('SignatureInput', () => import('./SignatureInput.vue'))
Vue.component('RichTextAreaInput', () => import('./RichTextAreaInput.vue')) Vue.component('RichTextAreaInput', () => import('./RichTextAreaInput.vue'))
Vue.component('DateInput', () => import('./DateInput.vue')) Vue.component('DateInput', () => import('./DateInput.vue'))
Vue.component('PhoneInput', () => import('./PhoneInput.vue'))

View File

@ -69,11 +69,14 @@
<script> <script>
import FormLogicPropertyResolver from '../../../forms/FormLogicPropertyResolver.js' import FormLogicPropertyResolver from '../../../forms/FormLogicPropertyResolver.js'
import FormPendingSubmissionKey from '../../../mixins/forms/form-pending-submission-key.js' import FormPendingSubmissionKey from '../../../mixins/forms/form-pending-submission-key.js'
import PhoneInput from '../../forms/PhoneInput.vue'
import {mapState} from "vuex"; import {mapState} from "vuex";
export default { export default {
name: 'OpenFormField', name: 'OpenFormField',
components: { }, components: {
PhoneInput
},
mixins: [FormPendingSubmissionKey], mixins: [FormPendingSubmissionKey],
props: { props: {
form: { form: {
@ -122,7 +125,7 @@ export default {
checkbox: 'CheckboxInput', checkbox: 'CheckboxInput',
url: 'TextInput', url: 'TextInput',
email: 'TextInput', email: 'TextInput',
phone_number: 'TextInput', phone_number: 'PhoneInput'
} }
}, },
/** /**
@ -246,7 +249,7 @@ export default {
placeholder: field.placeholder, placeholder: field.placeholder,
help: field.help, help: field.help,
helpPosition: (field.help_position) ? field.help_position : 'below_input', helpPosition: (field.help_position) ? field.help_position : 'below_input',
uppercaseLabels: this.form.uppercase_labels, uppercaseLabels: this.form.uppercase_labels == 1 || this.form.uppercase_labels == true,
theme: this.theme, theme: this.theme,
maxCharLimit: (field.max_char_limit) ? parseInt(field.max_char_limit) : 2000, maxCharLimit: (field.max_char_limit) ? parseInt(field.max_char_limit) : 2000,
showCharLimit: field.show_char_limit || false showCharLimit: field.show_char_limit || false

View File

@ -43,7 +43,7 @@ export default {
checkbox: 'CheckboxInput', checkbox: 'CheckboxInput',
url: 'TextInput', url: 'TextInput',
email: 'TextInput', email: 'TextInput',
phone_number: 'TextInput', phone_number: 'PhoneInput'
} }
} }
}, },

View File

@ -44,7 +44,7 @@ class FormSubmissionDataFactory
$value = $this->faker->url(); $value = $this->faker->url();
break; break;
case 'phone_number': case 'phone_number':
$value = $this->faker->phoneNumber(); $value = '+1 ' .$this->faker->phoneNumber();
break; break;
case 'date': case 'date':
$value = $this->faker->date(); $value = $this->faker->date();