Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

Armani's avatar
Level 17

Flatpickr Vuejs Componet

I created Flatpickr Vuejs Component and it works fine but when I use static to true as option it will wrap all template to a div with class: flatpickr-wrapper. it actually should wrap input with this div.

That's why I changes this.$el to this.$el.children[1], so now it works as expected.

I just wanted to know, is it normal to edit the this.$el? Is there any better solution?

<template>
  <div :class="'col-md-' + column">
    <label :for="name" :class="[required ? 'required' : null,'fw-bold fs-6 mb-3']">{{ label }}</label>
    <input :name="name" class="form-control form-control-solid mb-3 mb-lg-0" :id="name" :value="value" :required="required">
    <div class="fv-plugins-message-container invalid-feedback" v-if="error">{{ error }}</div>
  </div>
</template>

<script>
  // Events to emit, copied from flatpickr source
  const includedEvents = [
    'onChange',
    'onClose',
    'onDestroy',
    'onMonthChange',
    'onOpen',
    'onYearChange',
  ];

  // Let's not emit these events by default
  const excludedEvents = [
    'onValueUpdate',
    'onDayCreate',
    'onParseConfig',
    'onReady',
    'onPreCalendarPosition',
    'onKeyDown',
  ];
  const camelToKebab = (string) => {
    return string.replace(/([a-z])([A-Z])/g, '-').toLowerCase();
  };

  const arrayify = (obj) => {
    return obj instanceof Array ? obj : [obj];
  };

  const nullify = (value) => {
    return (value && value.length) ? value : null;
  }

  const cloneObject = (obj) => {
    return Object.assign({}, obj);
  };
  // Keep a copy of all events for later use
  const allEvents = includedEvents.concat(excludedEvents);

  // Passing these properties in `set()` method will cause flatpickr to trigger some callbacks
  const configCallbacks = ['locale', 'showMonths'];
  export default {
  name: 'flat-pickr',
  render(el) {
    return el('input', {
      attrs: {
        type: 'text',
        'data-input': true,
      },
      props: {
        disabled: this.disabled
      },
      on: {
        input: this.onInput
      }
    })
  },
  props: {
    value: {
      default: null,
      required: true,
      validator(value) {
        return (
          value === null ||
          value instanceof Date ||
          typeof value === "string" ||
          value instanceof String ||
          value instanceof Array ||
          typeof value === "number"
        );
      }
    },
    column: {
      type: String,
      default: '4'
    },
    required: {
      type: Boolean,
      default: false
    },
    label: {
      type: String,
      default: null
    },
    name: {
      type: String,
      default: null
    },
    // https://chmln.github.io/flatpickr/options/
    config: {
      type: Object,
      default: () => ({
        wrap: false,
        //defaultDate: 'today',
        static: true,
        dateFormat: "Y-m-d"
      })
    },
    events: {
      type: Array,
      default: () => includedEvents
    },
    disabled: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      /**
       * The flatpickr instance
       */
      fp: null,
      error: null
    };
  },
  mounted() {
    // Return early if flatpickr is already loaded
    /* istanbul ignore if */
    if (this.fp) return;

    // Don't mutate original object on parent component
    let safeConfig = cloneObject(this.config);

    this.events.forEach((hook) => {
      // Respect global callbacks registered via setDefault() method
      let globalCallbacks = flatpickr.defaultConfig[hook] || [];

      // Inject our own method along with user callback
      let localCallback = (...args) => {
        this.$emit(camelToKebab(hook), ...args)
      };

      // Overwrite with merged array
      safeConfig[hook] = arrayify(safeConfig[hook] || []).concat(globalCallbacks, localCallback);
    });

    // Set initial date without emitting any event
    safeConfig.defaultDate = this.value || safeConfig.defaultDate;

    // Init flatpickr
    this.fp = new flatpickr(this.getElem(), safeConfig);

    // Attach blur event
    this.fpInput().addEventListener('blur', this.onBlur);
    this.$on('on-close', this.onClose);
    this.$on('on-open', this.onOpen);

    // Immediate watch will fail before fp is set,
    // so need to start watching after mount
    this.$watch('disabled', this.watchDisabled, {immediate: true})

    if (typeof errors[this.name] !== 'undefined') {
      this.error = errors[this.name][0];
    }
  },
  methods: {
    /**
     * Get the HTML node where flatpickr to be attached
     * Bind on parent element if wrap is true
     */
    getElem() {
      return this.config.wrap ? this.$el.parentNode : this.$el.children[1]
    },

    /**
     * Watch for value changed by date-picker itself and notify parent component
     *
     * @param event
     */
    onInput(event) {
      const input = event.target;
      // Lets wait for DOM to be updated
      this.$nextTick(() => {
        this.$emit('input', nullify(input.value));
      });
    },

    /**
     * @return HTMLElement
     */
    fpInput() {
      return this.fp.altInput || this.fp.input;
    },

    /**
     * Blur event is required by many validation libraries
     *
     * @param event
     */
    onBlur(event) {
      this.$emit('blur', nullify(event.target.value));
    },

    onOpen(event) {
      if (!this.value) {
        this.fp.clear();
      }
    },

    /**
     * Flatpickr does not emit input event in some cases
     */
    onClose(selectedDates, dateStr) {
      this.$emit('input', nullify(dateStr))
    },

    /**
     * Watch for the disabled property and sets the value to the real input.
     *
     * @param newState
     */
    watchDisabled(newState) {
      if (newState) {
        this.fpInput().setAttribute('disabled', newState);
      } else {
        this.fpInput().removeAttribute('disabled');
      }
    }
  },
  watch: {
    /**
     * Watch for any config changes and redraw date-picker
     *
     * @param newConfig Object
     */
    config: {
      deep: true,
      handler(newConfig) {
        let safeConfig = cloneObject(newConfig);
        // Workaround: Don't pass hooks to configs again otherwise
        // previously registered hooks will stop working
        // Notice: we are looping through all events
        // This also means that new callbacks can not passed once component has been initialized
        allEvents.forEach((hook) => {
          delete safeConfig[hook];
        });
        this.fp.set(safeConfig);

        // Workaround: Allow to change locale dynamically
        configCallbacks.forEach((name) => {
          if (typeof safeConfig[name] !== 'undefined') {
            this.fp.set(name, safeConfig[name])
          }
        });
      }
    },

    /**
     * Watch for changes from parent component and update DOM
     *
     * @param newValue
     */
    value(newValue) {
      // Prevent updates if v-model value is same as input's current value
      if (newValue === nullify(this.$el.value)) return;
      // Make sure we have a flatpickr instance
      this.fp &&
      // Notify flatpickr instance that there is a change in value
      this.fp.setDate(newValue, true);
    },
  },
  /**
   * Free up memory
   */
  beforeDestroy() {
    /* istanbul ignore else */
    if (this.fp) {
      this.fpInput().removeEventListener('blur', this.onBlur);
      this.fp.destroy();
      this.fp = null;
    }
  },
};
</script>
0 likes
0 replies

Please or to participate in this conversation.