@econnect/webcomponents-library
Documentation
Styleguide
Documentation
Styleguide
  • Getting Started

    • Installation
    • Patch Notes
    • Styleguide
  • Styling

    • e-connect colors
  • UI Components

    • Form

      • Search Field
      • Form Field
      • Forms
    • Filter

      • Filter
      • Search Filter
    • Grid

      • Grid
      • Grid Row
    • Inputs

      • File Input
      • Input Checkbox
      • Input Dropdown
      • Input Date
      • Input Date Time
      • Input Date Time Local
      • Input File
      • Input Label
      • Input Multi Select
      • Input Number
      • Input Radio
      • Input Range
      • Input Select
      • Input Switch
      • Input Slider
      • Input Text
      • Input TextArea
      • Input Time
      • Input Time Range
    • Controls

      • Booleans
      • Button
      • Checkboxes
      • Date Displays
      • Dropdown Lists
      • Text Displays
    • Headers

      • e-header
    • Cards

      • e-card
    • Images

      • Icon
      • Images
      • Flag Image
      • Hamburger
    • Iframe
    • Pager
    • Settings
    • Document Validation
    • Navigation

      • Navigation
      • Navigation Menu
      • Navigation Footer
      • Navigation Button
      • Navigation Button Item
      • Navigation Search Item

Input Time Range v1.0

The <e-input-time-range> component is a component that can be used for .

Usage

An e-input-time-range has attributes that must be provided

<e-input-time-range
  id=""
  value=""
  label=""
  name=""
  placeholder=""
  required=""
  disabled=""
  options=""
  meta=""
/>

Attributes : e-input-time-range extends e-input

ValueTypeOptionalDefault
valueStringyes''
metaEInputMeta_TimeRangeyesnew EInputMeta_TimeRange()

Attributes : e-input

ValueTypeOptionalDefault
idStringyes''
valueStringyes''
labelStringyes''
nameStringyes''
placeholderStringyes''
requiredBooleanyesfalse
disabledBooleanyesfalse
optionsEFormFieldOptionType[]yes[]
metaEInputMetayes{ disabled: false }

Attributes : e-input-meta_time-range

ValueTypeOptionalDefault
disabledBooleanyesfalse
searchableBooleanyestrue
nullableBooleanyesfalse
customSplitterStringyes' ⇒ '
minDateRawDateStringyes'1970-01-01'
maxDateRawDateStringyes'9999-12-31'

Examples

Below a few interactive examples of e-input-time-range can be found.

Default
<e-input-time-range
  id="timerangeInputWithOptions"
  value="-"
/>
internal labelled, label="timerangeInput with Label"
<e-input-time-range
  id="timerangeInputWithLabel"
  label="timeRangeInput with Label"
  value="-"
/>
separate labelled, label="timerangeInput with Label"
<div class="input-example-field">
  <e-input-label
    id="timerangeInputWithExternalLabel"
    value="timeRangeInput with Label"
  />
  <e-input-time-range
    id="timerangeInputWithExternalLabel"
    value="-"
  />
</div>

Source Code

e-input-time-range.vue
<script lang="ts" setup>
import { computed, reactive, toRef, ref, unref, watch } from 'vue';
import { capitalize, cloneDeep, forEach, isNaN, map } from 'lodash-es';
import { useDateFormat } from '@vueuse/core';

import { useI18n } from 'vue-i18n';

import {
  useMetaUtils,
  ButtonTheme,
  ButtonType,
  EFormFieldModel,
  EFormFieldOptionType
} from 'types';

import { EInput_TimeRange } from 'types/e-input/interfaces/EInput_TimeRange';
import { EInputMeta_Select, EInputMeta_DateTimeLocal } from 'types/e-input/meta_interfaces';

import EButton from '../e-button/e-button.vue';
import EFormField from '../e-form-field/e-form-field.vue';

import { defaultSplitter, optionsList } from 'data/filters/options/timeRange';

import { isValidDate, getTimeRangeDates, getTimePeriodDates } from 'types/utils/dateUtils';

export type Props = EInput_TimeRange;
const props = withDefaults(defineProps<Props>(), {
  value: '',
  id: '',
  label: '',
  placeholder: '',
  required: false,
  disabled: false,
  options: () => {
    return [] as EFormFieldOptionType[];
  },
  meta: () => {
    return {
      disabled: false,
      searchable: true,
      nullable: false
    };
  }
});

const emit = defineEmits<{
  (e: 'blur', event: FocusEvent): void;
  (e: 'keyup.enter', event: KeyboardEvent): void;
  (e: 'update:value', value: string): void;
}>();

const { t, te } = useI18n({ useScope: 'global' });

const customMenuTitle = ref(t('timeRange.giveInput'));
const localPattern = ref('YYYY-MM-DDTHH:mm');

const localValue = ref(props.value || '-');
const showDropdown = ref(false);
const isValid = ref(true);

const metaData = computed(() => useMetaUtils(toRef(props.meta)));
const isDisabled = computed(() => unref(metaData).isDisabled.value);
const minDate = computed(() => new Date(props.meta?.minDate || '1970-01-01'));
const maxDate = computed(() => new Date(props.meta?.maxDate || '9999-12-31'));

const customSplitter = computed(() => props.meta.customSplitter ?? defaultSplitter);

const dates = reactive({
  default: {
    start: new Date(new Date().setMonth(new Date().getMonth() - 1)),
    end: new Date()
  },
  custom: {
    start: new Date(new Date().setMonth(new Date().getMonth() - 1)),
    end: new Date()
  },
  result: {
    start: new Date(new Date().setMonth(new Date().getMonth() - 1)),
    end: new Date()
  }
});
const formattedDates = computed(() => {
  return {
    start: useDateFormat(dates.custom.start, localPattern),
    end: useDateFormat(dates.custom.end, localPattern)
  };
});

const customResult = computed(
  () => unref(formattedDates.value.start) + customSplitter.value + unref(formattedDates.value.end)
);
const containsSplitter = computed(() => customResult.value?.includes(customSplitter.value));

const canSubmitCustom = computed(
  () => isValid.value && !isNaN(dates.custom.start) && !isNaN(dates.custom.end)
);

const formattedTimeRange = computed(() => {
  const start = useDateFormat(dates.result.start, localPattern);
  const end = useDateFormat(dates.result.end, localPattern);

  return unref(start) + customSplitter.value + unref(end);
});

const baseTimeRangeOptions = computed(() => {
  return map(optionsList, option => {
    const translatedPeriod =
      option.label.period && te(option.label.period) ? t(option.label.period) : '';
    const translatedFormat =
      option.label.format && te(option.label.format)
        ? t(option.label.format, { count: option.label?.count })
        : '';

    const translationParams = {
      period: translatedPeriod,
      count: option.label?.count,
      format: translatedFormat
    };
    const label = te(option.label.key) ? t(option.label.key, translationParams) : option.value;

    return {
      label: label,
      value: option.value,
      disabled: option.disabled,
      hidden: option.hidden
    };
  });
});

const timeRangeOptions = computed(() => {
  const result = cloneDeep(baseTimeRangeOptions.value);
  const optionIndex = result.findIndex(
    o => o.value.toLowerCase() === customResult.value.toLowerCase()
  );

  // Add custom options
  const optionList: EFormFieldOptionType[] = props.options || [];
  forEach(optionList, option => {
    const labelValue = option.label || option.value;

    result.push({
      id: option.id,
      label: te(labelValue) ? t(labelValue) : labelValue,
      value: option.value,
      disabled: option.disabled,
      hidden: option.hidden
    });
  });

  if (containsSplitter.value || (localValue.value === 'custom' && optionIndex === -1)) {
    result.push({
      value: customResult.value,
      label: customResult.value,
      hidden: true
    });
  }

  return result;
});

const timeRangeOptionlist = computed(() => {
  return map(timeRangeOptions.value, timeRange => {
    return {
      id: timeRange.value,
      text: timeRange.label,
      value: timeRange.value,
      disabled: false,
      hidden: timeRange.hidden
    } as EFormFieldOptionType;
  });
});

const selectFormFieldModel = computed(() => {
  return {
    type: 'select',
    id: props.id,
    value: localValue.value,
    label: props.label,
    showLabel: props.label !== undefined,
    placeholder: props.placeholder,
    required: props.required,
    hidden: false,
    meta: {
      ...props.meta
    } as EInputMeta_Select,
    options: timeRangeOptionlist.value
  } as EFormFieldModel;
});

const startDateValue = ref(unref(formattedDates.value.start));
const startDateFormFieldModel = computed(() => {
  const label = capitalize(t('timeRange.input.start', [t('timeRange.format.time')]));

  return {
    type: 'datetime-local',
    id: 'startDate',
    value: startDateValue.value,
    label: label,
    showLabel: true,
    placeholder: props.placeholder,
    required: false,
    hidden: false,
    meta: {
      timeDisabled: false,
      requireTime: false,
      minDate: minDate.value,
      maxDate: maxDate.value
    } as EInputMeta_DateTimeLocal,
    options: []
  } as EFormFieldModel;
});

const endDateValue = ref(unref(formattedDates.value.end));
const endDateFormFieldModel = computed(() => {
  const label = capitalize(t('timeRange.input.end', [t('timeRange.format.time')]));

  return {
    type: 'datetime-local',
    id: 'endDate',
    value: endDateValue.value,
    label: label,
    showLabel: false,
    placeholder: props.placeholder,
    required: false,
    hidden: false,
    meta: {
      timeDisabled: false,
      requireTime: false,
      minDate: minDate.value,
      maxDate: maxDate.value
    } as EInputMeta_DateTimeLocal,
    options: []
  } as EFormFieldModel;
});

const stateClasses = computed(() => {
  return {
    required: props.required,
    disabled: isDisabled.value
  };
});

function setValidity(data: { key: string; newValue: boolean }) {
  isValid.value = data.newValue;
}

function setDateValue(data: { key: string; newValue: string }) {
  const newDate = data.newValue ? new Date(data.newValue) : null;

  switch (data.key) {
    case 'startDate':
      dates.custom.start = newDate;
      break;
    case 'endDate':
      dates.custom.end = newDate;
      break;
  }
}

function optionSelected(data: { key: string; newValue: string }) {
  showDropdown.value = data.newValue === 'custom';

  if (!showDropdown.value) {
    localValue.value = data.newValue;

    emit('update:value', data.newValue);
  }
}

function setResult() {
  showDropdown.value = false;

  updateInputRefs();

  dates.result.start = dates.custom.start;
  dates.result.end = dates.custom.end;
}

function updateInputRefs() {
  startDateValue.value = unref(useDateFormat(dates.custom.start, localPattern));
  endDateValue.value = unref(useDateFormat(dates.custom.end, localPattern));
}

// Update default date values, ensure they're valid dates.
function updateDefaultValues(newDates: { start: Date; end: Date }) {
  if (isValidDate(newDates.start)) dates.default.start = cloneDeep(newDates.start);

  if (isValidDate(newDates.end)) dates.default.end = cloneDeep(newDates.end);
}

function emitCustomRange() {
  localValue.value = customResult.value;

  if (isValidDate(dates.custom.start) && isValidDate(dates.custom.end)) {
    setResult();
    localValue.value = formattedTimeRange.value;

    emit('update:value', localValue.value);
  }
}

function resetLocalValue() {
  localValue.value = props.value || '-';

  showDropdown.value = localValue.value === 'custom';
}

function setDateRange(newDateRange: string) {
  const _dates = newDateRange?.split(customSplitter.value);
  const newDates =
    _dates?.length > 1 ? getTimeRangeDates(newDateRange) : getTimePeriodDates(newDateRange);

  dates.custom.start = newDates.start;
  dates.custom.end = newDates.end;

  updateInputRefs();

  updateDefaultValues(newDates);

  localValue.value = newDateRange;
}

watch(
  () => props.value,
  newValue => {
    if (newValue) {
      setDateRange(newValue);
    }
  },
  {
    immediate: true
  }
);
</script>

<template>
  <div
    class="e-input-time-range"
    :class="stateClasses"
  >
    <div
      class="input-container"
      role="none"
    >
      <e-form-field
        :model="selectFormFieldModel"
        @emit:value="optionSelected"
      />

      <div
        v-if="showDropdown"
        class="time-range-selection"
        role="combobox"
        aria-labelledby="custom-time-range-menu"
      >
        <div
          class="dropdown-container"
          role="none"
        >
          <label
            id="custom-time-range-menu"
            :title="customMenuTitle"
            :aria-label="customMenuTitle"
            v-text="customMenuTitle"
          />

          <div class="inputs">
            <e-form-field
              :model="startDateFormFieldModel"
              @emit:validity="setValidity"
              @emit:value="setDateValue"
            />
            <e-form-field
              :model="endDateFormFieldModel"
              @emit:validity="setValidity"
              @emit:value="setDateValue"
            />
          </div>

          <div class="functions">
            <e-button
              :type="ButtonType.Default"
              :text="t('functions.ok')"
              :theme="ButtonTheme.Primary"
              :disabled="!canSubmitCustom"
              @clicked="emitCustomRange"
            />
            <e-button
              :type="ButtonType.Reset"
              :text="t('functions.cancel')"
              @click="resetLocalValue"
            />
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped lang="scss">
.e-input-time-range {
  align-items: center;
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  border: none;
  position: relative;
  gap: 10px;

  .current-value-label {
    min-width: 60px;
  }

  .input-container {
    align-items: flex-start;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    flex: 1 1 auto;
    position: relative;
    border-radius: 15px;

    .e-form-field {
      display: flex;
      width: 100%;
      flex-direction: row;
      flex: 1 1 auto;
      align-items: center;

      :deep(label) {
        width: 30%;
      }
      :deep(input) {
        flex: 1 1 auto;
      }

      :deep(.e-input-select) {
        width: 70%;
        min-width: 200px;
      }
    }

    label {
      min-width: 30px;
    }

    .time-range-selection {
      position: relative;
      z-index: 9000;
      left: calc(50% - 5px);

      .dropdown-container {
        border-radius: 15px;
        border: 1px solid var(--e-foreground);
        background-color: var(--e-background);
        padding: 10px;
        position: absolute;
        top: 20px;
        display: flex;
        flex-direction: column;

        &::before {
          background-color: var(--e-foreground);
          content: '';
          display: inline-block;
          border: 1px solid var(--e-foreground);
          position: absolute;
          left: 50%;
          top: -25px;
          height: 20px;
          width: 20px;
          transform: translateX(-50%) translateY(100%) rotate(45deg);
          z-index: -1;
        }

        label {
          padding: 10px 20px;
          margin: 0;
        }
        .inputs {
          display: flex;
          flex-direction: column;
          gap: 10px;

          :user-invalid {
            color: var(--e-red);
          }
        }

        .e-form-field {
          :deep(label) {
            max-width: 80px;
          }
        }

        .functions {
          display: flex;
          justify-content: flex-end;
          gap: 10px;
          padding: 15px;

          .e-button {
            width: 100px;
            text-align: center;
          }
        }
      }
    }
  }

  .field-row {
    display: flex;
    flex-direction: column;
  }

  .date-row {
    display: flex;
    flex-direction: column;

    label {
      padding: 0 20px;
    }

    .inputs {
      display: flex;
      flex-direction: row;
      padding: 5px 15px;
      gap: 10px;

      .e-input {
        background-color: var(--e-input-background);
        padding: 5px;
        border-radius: 10px;
        border: none;
        flex: 1 1 auto;
      }
    }
  }

  &.disabled {
    .input-container {
      label {
        cursor: not-allowed;
        user-input: none;
      }
    }
  }
}
</style>
Last Updated:: 2/19/25, 2:50 PM
Contributors: Marcel Lommers, Antony Elfferich
Prev
Input Time