This was logic written in a pregnancy scheduling mini web app that I wanted to separate the date range out.
Created
January 13, 2023 14:39
-
-
Save renoirb/3ac53c986c0284d6f0be585cc5bac3c9 to your computer and use it in GitHub Desktop.
Date Range component logic
This file contains hidden or 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> | |
<label for="min"> Begin </label> | |
<input | |
:max="distanceDates.max.value" | |
name="min" | |
type="date" | |
v-model="distanceDates.min.value" | |
/> | |
<span class="validity"></span> | |
<label for="max"> End </label> | |
<input | |
:min="distanceDates.min.value" | |
name="max" | |
type="date" | |
v-model="distanceDates.max.value" | |
/> | |
<span class="validity"></span> | |
</fieldset> | |
</template> | |
<script lang="ts"> | |
import { useDistanceDates } from './use-distance-dates' | |
// reminder: this is scavenged code from a 3 y.o. Project when Vue 3 was still in beta. | |
// also. It's been 2 years I don't write Vue full time | |
// and am in parental leave, writing this on an iPhone while baby is napping! | |
// (Yeah. I miss programming!) | |
export default { | |
name: 'DateRange', | |
setup() { | |
const { distanceDates } = useDistanceDates() | |
// the onMounted and watchEffect should be initialized | |
return { | |
distanceDates, | |
} | |
}, | |
} | |
</script> | |
<style scoped> | |
label { | |
display: flex; | |
align-items: center; | |
} | |
span::after { | |
padding-left: 5px; | |
} | |
input:invalid + span::after { | |
content: '✖'; | |
} | |
input:valid + span::after { | |
content: '✓'; | |
} | |
</style> |
This file contains hidden or 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
import { | |
reactive, | |
onMounted, | |
watchEffect, | |
toRefs, | |
ToRefs, | |
unref, | |
ref, | |
readonly, | |
} from 'vue' | |
import { DateTime, Interval, DurationUnit } from 'luxon' | |
export interface IDateRange { | |
max: string | |
min: string | |
} | |
export interface IDistanceDatesSurface { | |
distanceDates: ToRefs<IDateRange> | |
duration: Ref<number> | |
mutate(changeset: Partial<IDateRange>): void | |
} | |
// reminder: The original code had more than those two fields. | |
const distanceDatesModelFields = new Set([ | |
'max', | |
'min', | |
]) | |
export const isInDistanceDatesModelFields = (field: string): boolean => | |
distanceDatesModelFields.has(field) | |
export const isDateTimeString = (input: unknown): input is DateTime => { | |
let outcome = false | |
try { | |
const _ = DateTime.fromISO(input as string) | |
outcome = true | |
} catch { | |
// Nothing to do | |
} | |
return outcome | |
} | |
export const useDistanceDates = ( | |
): IDistanceDatesSurface => { | |
const duration = ref(0) | |
const today = DateTime.local().toISODate() | |
const distanceDates = reactive({ | |
max: today, | |
min: today, | |
} as IDateRange) | |
// this method was doing more than now just | |
// changing min, max dates. | |
// reminder: this was edited code on an iPhone from 3 y.o. hobby project | |
const mutate = (changeset: Partial<IDateRange> = {}): void => { | |
// Here there was logic about whether min or max be as today's date | |
for (const [key, value] of Object.entries(changeset)) { | |
if (distanceDatesModelFields.has(key)) { | |
// distanceDates[key] = value | |
Reflect.set(distanceDates, key, value) | |
} | |
} | |
} | |
// should one want to use URL search ?min=2023-01-11 | |
onMounted(() => { | |
// this assumes your view would ONLY support min,max URL Search once. | |
const { search = '' } = window.location | |
const parsed = String(search || '') | |
.replace(/^\?/, '') | |
.split('&') | |
.map((i) => i.split('=')) | |
const changeset: Partial<IDateRange> = {} | |
for (const [key, value] of parsed) { | |
if (distanceDatesModelFields.has(key)) { | |
// validate "value" as yyyy-mm-dd | |
// yeah. It's a Date without Time. | |
// This is 3 years old code! | |
// I realize I'll have to change that | |
if (!isDateTimeString(value)){ | |
// do nothing or error out! TODO | |
} else { | |
// prefer reflect for changing value | |
// changeset[key] = value | |
Reflect.set(changeset, key, value) | |
} | |
} | |
} | |
mutate(changeset) | |
}) | |
watchEffect(() => { | |
const max = DateTime.fromISO(distanceDates.max) | |
const min = DateTime.fromISO(distanceDates.min) | |
const interval = Interval.fromDateTimes(min, max) | |
// should you want to use another duration than days. | |
// see Luxon docs! | |
const unit: DurationUnit = 'days' | |
const computedDuration = Math.ceil(interval.length(unit)) | |
duration.value = computedDuration | |
}) | |
// https://github.com/vuejs/vue-next/blob/master/packages/reactivity/__tests__/effect.spec.ts#L117 | |
return { | |
mutate, | |
distanceDates: toRefs(distanceDates), | |
duration: readonly(duration), | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment