Skip to content

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>