Multi range
This component combines two Range components, adds some extra CSS for the inverted one (which sits on top of the other) and takes min/max values as "boundaries" and uses toDate/fromDate as model values.
Live example
Usage
vue
<script setup>
import { ref } from 'vue'
import { MultiRange } from '@tickster/ui-framework/library.mjs'
const from1 = ref("2024-07-01")
const to1 = ref("2024-07-31")
</script>
<template>
<multi-range
:min="`2024-01-01`"
:max="`2024-12-31`"
v-model:from-date="from1"
v-model:to-date="to1"
@updated="console.log('updated multi range 1')" />
</template>vue
<script setup>
import { ref } from 'vue'
import { MultiRange } from '@tickster/ui-framework/library.mjs'
const from2 = ref("2024-04-01")
const to2 = ref("2024-07-31")
</script>
<template>
<multi-range
:min="`2024-01-01`"
:max="`2024-12-31`"
v-model:from-date="from2"
v-model:to-date="to2"
@updated="console.log('updated multi range 2')">
<div class="o-grid o-grid--bleed date-span">
<div class="o-grid__col-6">
<div class="c-popup c-popup--top-left u-box-shadow--none">
<div class="c-popup__item c-form-control">
<label for="from-date" class="c-label">From date</label>
<input id="from-date" type="date" v-model="from2" /> <!-- :min and :max props must be added here, as milliseconds, to enable dynamic data boundaries on the datepicker as well -->
</div>
</div>
</div>
<div class="o-grid__col-6">
<div class="c-popup c-popup--top-right u-box-shadow--none">
<div class="c-popup__item c-form-control">
<label for="to-date" class="c-label">To date</label>
<input id="to-date" type="date" v-model="to2" /> <!-- :min and :max props must be added here, as milliseconds, to enable dynamic data boundaries on the datepicker as well -->
</div>
</div>
</div>
</div>
</multi-range>
</template>Component source
vue
<script setup>
import { computed, watch } from 'vue'
const emit = defineEmits(["updated"]);
const props = defineProps({
min: { type: String, required: true },
max: { type: String, required: true },
});
const fromDate = defineModel('fromDate', { type: String, required: true });
const toDate = defineModel('toDate', { type: String, required: true });
const stepAsDays = 1000 * 60 * 60 * 24;
const minDateAsNumber = computed(() => Number(new Date(props.min).getTime()));
const maxDateAsNumber = computed(() => Number(new Date(props.max).getTime()));
const fromDateAsNumber = computed({
get: () => Number(new Date(fromDate.value).getTime()),
set: (value) => {
fromDate.value = dateAsYyyyMmDd(value);
}
});
const toDateAsNumber = computed({
get: () => Number(new Date(toDate.value).getTime()),
set: (value) => {
toDate.value = dateAsYyyyMmDd(value);
}
});
let isFirstLoad = true;
function dateAsYyyyMmDd(dateInMs) {
if (!dateInMs)
return
return new Date(parseInt(dateInMs)).toJSON().substring(0, 10);
}
// Make sure we're in valid date constraints
watch(fromDate, (val) => {
if (!isFirstLoad)
emit("updated", fromDate.value, toDate.value);
// If the from date is greater than the to date, set the to date to the from date
if (val > toDate.value)
fromDate.value = toDate.value;
if (val != props.min)
isFirstLoad = false;
});
watch(toDate, (val) => {
if (!isFirstLoad)
emit("updated", fromDate.value, toDate.value);
// If the to date is less than the from date, set the from date to the to date
if (val < fromDate.value)
toDate.value = fromDate.value;
if (val != props.max)
isFirstLoad = false;
});
</script>
<template>
<div class="c-multi-range">
<input type="range" :min="minDateAsNumber" :max="maxDateAsNumber" :step="stepAsDays" v-model="fromDateAsNumber" class="c-range c-range--inverted" :class="{ 'c-range--min': toDateAsNumber === minDateAsNumber }" :style="`--min: ${minDateAsNumber}; --val: ${fromDateAsNumber}; --max: ${maxDateAsNumber}` " />
<input type="range" :min="minDateAsNumber" :max="maxDateAsNumber" :step="stepAsDays" v-model="toDateAsNumber" class="c-range" :style="`--min: ${minDateAsNumber}; --val: ${toDateAsNumber}; --max: ${maxDateAsNumber}`" />
<slot></slot>
</div>
</template>