Created
October 12, 2021 00:22
-
-
Save jsonberry/7b6a3b29b222505d5bdfa0e446ac8fe7 to your computer and use it in GitHub Desktop.
Vue Native Platform Date Component Strategy
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<fieldset class="date-input" :data-qa-id="id"> | |
<legend>{{ legend }}</legend> | |
<component | |
@change="onChange($event)" | |
:is="component" | |
:custom-validity="customValidity" | |
v-bind="$props" | |
/> | |
</fieldset> | |
</template> | |
<script lang="ts"> | |
import { defineComponent, onBeforeMount, ref } from '@vue/composition-api'; | |
import compareAsc from 'date-fns/compareAsc'; | |
import format from 'date-fns/format'; | |
import parse from 'date-fns/parse'; | |
import Default from './DefaultDateInput.vue'; | |
import Fallback from './FallbackDateInput.vue'; | |
export default defineComponent({ | |
components: { | |
Default, | |
Fallback, | |
}, | |
props: ['name', 'id', 'legend', 'inputValue', 'required', 'min', 'max'], | |
setup(props, { emit }) { | |
const component = ref(Default); | |
const customValidity = ref(null); | |
onBeforeMount(() => { | |
const test = document.createElement('input'); | |
try { | |
test.type = 'date'; | |
} catch (e) { | |
console.log(e.description); | |
} | |
if (test.type === 'text') { | |
component.value = Fallback; | |
} | |
}); | |
const updateValidity = newValue => { | |
let validMin = true; | |
let validMax = true; | |
let valueDate: Date; | |
let minDate: Date; | |
let maxDate: Date; | |
try { | |
valueDate = parse(newValue, 'yyyy-MM-dd', new Date()); | |
} catch (re) { | |
console.warn('DateInput new value not parseable', newValue); | |
} | |
if (!valueDate) { | |
return; | |
} | |
if (props.min?.length) { | |
minDate = parse(props.min, 'yyyy-MM-dd', new Date()); | |
validMin = compareAsc(valueDate, minDate) !== -1; // -1 if valueDate is before minDate | |
} | |
if (props.max?.length) { | |
maxDate = parse(props.max, 'yyyy-MM-dd', new Date()); | |
validMax = compareAsc(valueDate, maxDate) !== 1; // 1 if valueDate is after maxDate | |
} | |
if (!validMin) { | |
customValidity.value = `Must be on or after ${format( | |
minDate, | |
'MM/dd/yyyy', | |
)}`; | |
} else if (!validMax) { | |
customValidity.value = `Must be on or before ${format( | |
maxDate, | |
'MM/dd/yyyy', | |
)}`; | |
} else { | |
customValidity.value = ''; | |
} | |
}; | |
return { | |
component, | |
customValidity, | |
onChange: newValue => { | |
updateValidity(newValue); | |
emit('change', newValue); | |
}, | |
}; | |
}, | |
}); | |
</script> | |
<style lang="scss" scoped> | |
fieldset { | |
width: 100%; | |
border: rem(2) solid $md-dol-divider; | |
border-radius: rem(8); | |
padding: rem(8); | |
padding-top: rem(4); | |
} | |
legend { | |
@include md-caption; | |
color: $md-dol-secondary; | |
} | |
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<input | |
@change="$emit('change', $event.target.value)" | |
:required="required" | |
:id="id" | |
:name="name" | |
:value="inputValue" | |
:min="min" | |
:max="max" | |
type="date" | |
ref="input" | |
/> | |
</template> | |
<script lang="ts"> | |
import { defineComponent, ref, watch } from '@vue/composition-api'; | |
export default defineComponent({ | |
props: [ | |
'name', | |
'id', | |
'placeholder', | |
'inputValue', | |
'required', | |
'pattern', | |
'min', | |
'max', | |
'customValidity', | |
], | |
setup(props) { | |
const input = ref(null); | |
watch( | |
() => props.customValidity, | |
curr => { | |
if (input.value) { | |
input.value.setCustomValidity(curr); | |
} | |
}, | |
); | |
return { input }; | |
}, | |
}); | |
</script> | |
<style lang="scss" scoped> | |
input { | |
width: 100%; | |
background-color: $gray-1; | |
} | |
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<template> | |
<section ref="componentEl"> | |
<select :required="required" v-model="month" name="month" id="month-picker"> | |
<option value="-1">--</option> | |
<option | |
v-for="{ value, content } in months" | |
:value="value" | |
:key="value" | |
>{{ content }}</option | |
> | |
</select> | |
<select :required="required" v-model="day" name="day" id="day-picker"> | |
<option value="0">--</option> | |
<option v-for="day in days" :key="day">{{ day }}</option> | |
</select> | |
<select v-model="year" :required="required" ref="yearSelect"> | |
<option value="0">--</option> | |
<option v-for="year in years" :key="year">{{ year }}</option> | |
</select> | |
</section> | |
</template> | |
<script lang="ts"> | |
import { computed, defineComponent, ref, watch } from '@vue/composition-api'; | |
import getDaysInMonth from 'date-fns/getDaysInMonth'; | |
import Default from './DefaultDateInput.vue'; | |
import Fallback from './FallbackDateInput.vue'; | |
import format from 'date-fns/format'; | |
export default defineComponent({ | |
components: { | |
Default, | |
Fallback, | |
}, | |
props: [ | |
'name', | |
'id', | |
'legend', | |
'inputValue', | |
'required', | |
'min', | |
'max', | |
'customValidity', | |
], | |
setup(props, { emit }) { | |
const yearSelect = ref(null); | |
const componentEl = ref(null); | |
const selectedDate = ref(null); | |
const formattedDate = ref(null); | |
const today = new Date(); | |
const [y, m, d] = (props?.inputValue?.length && | |
props.inputValue.split('-').map(v => parseInt(v, 10))) || [ | |
today.getUTCFullYear(), | |
0, | |
0, | |
]; | |
const month = ref(m - 1); | |
const day = ref(d); | |
const year = ref(y); | |
const months = [ | |
{ value: 0, content: 'January' }, | |
{ value: 1, content: 'February' }, | |
{ value: 2, content: 'March' }, | |
{ value: 3, content: 'April' }, | |
{ value: 4, content: 'May' }, | |
{ value: 5, content: 'June' }, | |
{ value: 6, content: 'July' }, | |
{ value: 7, content: 'August' }, | |
{ value: 8, content: 'September' }, | |
{ value: 9, content: 'October' }, | |
{ value: 10, content: 'November' }, | |
{ value: 11, content: 'December' }, | |
]; | |
const days = computed(() => { | |
if (isNaN(year.value) || isNaN(month.value)) { | |
return []; | |
} | |
return Array(getDaysInMonth(new Date(year.value, month.value))) | |
.fill(0) | |
.map((_, i) => i + 1); | |
}); | |
// Calculate the years for the dropdown. If `min` or `max` isn't specified | |
// set the missing option to +/- 150 | |
const years = computed(() => { | |
let start = new Date(props.min).getUTCFullYear(); | |
let stop = new Date(props.max).getUTCFullYear(); | |
if (isNaN(start)) { | |
start = today.getUTCFullYear() - 150; | |
} | |
if (isNaN(stop)) { | |
stop = today.getUTCFullYear() + 150; | |
} | |
return Array(stop - start + 1) | |
.fill(0) | |
.map((_, i) => start + i) | |
.reverse(); | |
}); | |
watch([month, day, year], (curr: number[]) => { | |
const [month, day, year] = curr.map((n: string | number): number => { | |
if (typeof n === 'string') { | |
return parseInt(n, 10); | |
} | |
return n; | |
}); | |
if (month === -1 || day === 0 || year === 0) { | |
emit('change', ''); | |
return; | |
} | |
selectedDate.value = new Date(year, month, day); | |
formattedDate.value = format(selectedDate.value, 'yyyy-MM-dd'); | |
emit('change', formattedDate.value); | |
}); | |
watch( | |
() => props.customValidity, | |
curr => { | |
if (yearSelect.value) { | |
yearSelect.value.setCustomValidity(curr); | |
} | |
}, | |
); | |
return { | |
componentEl, | |
yearSelect, | |
month, | |
months, | |
day, | |
days, | |
year, | |
years, | |
}; | |
}, | |
}); | |
</script> | |
<style lang="scss" scoped> | |
select { | |
margin-right: rem(8); | |
&:focus::-ms-value { | |
color: black; | |
background: transparent; | |
} | |
} | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment