Skip to content

Instantly share code, notes, and snippets.

@jsonberry
Created October 12, 2021 00:22
Show Gist options
  • Save jsonberry/7b6a3b29b222505d5bdfa0e446ac8fe7 to your computer and use it in GitHub Desktop.
Save jsonberry/7b6a3b29b222505d5bdfa0e446ac8fe7 to your computer and use it in GitHub Desktop.
Vue Native Platform Date Component Strategy
<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>
<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>
<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