primeng icon indicating copy to clipboard operation
primeng copied to clipboard

Mask support for Calendar

Open vadjs opened this issue 9 years ago β€’ 57 comments

I'm submitting a ... (check one with "x")

[ ] bug report => Search github for a similar issue or PR before submitting
[x] feature request => Please check if request is not on the roadmap already https://github.com/primefaces/primeng/wiki/Roadmap
[ ] support request => Please do not submit support request here, instead see http://forum.primefaces.org/viewforum.php?f=35

Current behavior

Now InputMask is not possible to be used in calendar input.

Expected behavior

Will be great if calendar will have option that will allow to use InputMask based on the dateFormat option.

What is the motivation / use case for changing the behavior?

That feature will improve user experience when he inputs data from the keyboard.

vadjs avatar Nov 25 '16 15:11 vadjs

After this issue will be resolved, we'll consider switching from Wijmo to PrimeNG. As of now we use both.

yfain avatar Dec 16 '16 11:12 yfain

In our enterprise environment, we currently use another third-party mask plugin (angular2-text-mask) in tandem with the PrimeNG calendar control. While we've managed to make this work, the solution is not ideal and often introduces bugs that are difficult to resolve.

An option on the PrimeNG calendar to apply an input mask would be much appreciated. It might be even more ideal if the PrimeNG input mask was just a dynamic directive that could be applied to either native inputs or other PrimeNG components, including the calendar/datepicker.

Da13Harris avatar Oct 04 '17 17:10 Da13Harris

@Da13Harris I added a issue that describe your request. Change InputMask component to a directive

sousajunior avatar Oct 09 '17 16:10 sousajunior

anyone found a solution for this issue ?

UlricW avatar Apr 05 '18 08:04 UlricW

πŸ‘

leonardolessa avatar Apr 10 '18 19:04 leonardolessa

if anyone found solution for this issue then please share it with me .

sandeepGhotra avatar May 08 '18 12:05 sandeepGhotra

πŸ‘

thenninger avatar May 18 '18 14:05 thenninger

so noth no changed?

3em avatar May 29 '18 12:05 3em

πŸ‘

Choubal avatar Jun 01 '18 14:06 Choubal

Is this planned for any release?

vinothbabu avatar Jul 10 '18 09:07 vinothbabu

I'm already getting negative feedback from my company about the calendars' lack of an input mask. Please add this feature!

thenninger avatar Jul 25 '18 13:07 thenninger

Is it possible to have a feedback to know if this "new feature" is planned or not ? cause it's an important feature for a lot of people..

UlricW avatar Aug 17 '18 07:08 UlricW

I did a paleative solution without a third-party mask plugin using the event the component send. My code is fresh, so it needs to be worked out. I did the algoritm by intercepting the input element (event.path[0]):

<p-calendar
    ...
    dateFormat="dd/mm/yy"
    (onInput)="onInputDate($event)" (onBlur)="onBlurDate()"
    placeholder="dd/mm/aaaa" [locale]="ptBR"
    ...
></p-calendar>
    onInputDate(event): void {
        let cursorPosition = event.path[0].selectionEnd;

        if (event.inputType === 'deleteContentBackward' && (cursorPosition === 2 || cursorPosition === 5)) {
            event.path[0].value = event.path[0].value.substring(0, cursorPosition - 1) + event.path[0].value.substring(cursorPosition);
            cursorPosition --;
        }
        if (event.inputType === 'insertText' && (event.path[0].value.length > 10)) {
            event.path[0].value = event.path[0].value.substring(0, event.path[0].value.length - 1);
        }

        this.dateMask = event.path[0].value.toString();
        this.dateMask = this.dateMask.replace(/\D/g, '');

        let mask = '';
        for (let i = 0; i < this.dateMask.length; i++) {
            mask += this.dateMask[i];
            if (i === 1 || i === 3) {
                mask += '/';
                if (cursorPosition === 2 || cursorPosition === 5) { cursorPosition++; }
            }
        }
        event.path[0].value = mask.toString();
        event.path[0].selectionStart = cursorPosition;
        event.path[0].selectionEnd = cursorPosition;

        if (event.path[0].value.length === 10) {
            const dt = this.stringToDate(event.path[0].value);
            if (this.isValidDate(dt)) {
                this.value = dt;
            }
        }
    }

    onBlurDate(): void {
        if (!!this.value && !this.isValidDate(this.value)) {
            this.value = null;
        }
    }

I know that is not a good looking solution, but maybe it could help some one.

icarodebarros avatar Sep 17 '18 11:09 icarodebarros

Any updated on this issue? @cagataycivici

xandecf avatar Oct 19 '18 14:10 xandecf

πŸ‘

anafreitas avatar Oct 30 '18 12:10 anafreitas

I did a paleative solution without a third-party mask plugin using the event the component send. My code is fresh, so it needs to be worked out. I did the algoritm by intercepting the input element (event.path[0]):

<p-calendar
    ...
    dateFormat="dd/mm/yy"
    (onInput)="onInputDate($event)" (onBlur)="onBlurDate()"
    placeholder="dd/mm/aaaa" [locale]="ptBR"
    ...
></p-calendar>
    onInputDate(event): void {
        let cursorPosition = event.path[0].selectionEnd;

        if (event.inputType === 'deleteContentBackward' && (cursorPosition === 2 || cursorPosition === 5)) {
            event.path[0].value = event.path[0].value.substring(0, cursorPosition - 1) + event.path[0].value.substring(cursorPosition);
            cursorPosition --;
        }
        if (event.inputType === 'insertText' && (event.path[0].value.length > 10)) {
            event.path[0].value = event.path[0].value.substring(0, event.path[0].value.length - 1);
        }

        this.dateMask = event.path[0].value.toString();
        this.dateMask = this.dateMask.replace(/\D/g, '');

        let mask = '';
        for (let i = 0; i < this.dateMask.length; i++) {
            mask += this.dateMask[i];
            if (i === 1 || i === 3) {
                mask += '/';
                if (cursorPosition === 2 || cursorPosition === 5) { cursorPosition++; }
            }
        }
        event.path[0].value = mask.toString();
        event.path[0].selectionStart = cursorPosition;
        event.path[0].selectionEnd = cursorPosition;

        if (event.path[0].value.length === 10) {
            const dt = this.stringToDate(event.path[0].value);
            if (this.isValidDate(dt)) {
                this.value = dt;
            }
        }
    }

    onBlurDate(): void {
        if (!!this.value && !this.isValidDate(this.value)) {
            this.value = null;
        }
    }

I know that is not a good looking solution, but maybe it could help some one.

@icarodebarros This did help me - I just had to change event.path[0] to event.target because only Chrome supports path currently as it is not standard. I also did not need to use the blur event.

thenninger avatar Nov 27 '18 14:11 thenninger

Any news about this?

ghost avatar Dec 18 '18 16:12 ghost

Nothing about this yet??

mikaelboff avatar Jan 31 '19 20:01 mikaelboff

this is so annoying. should be a standard.

tayloraharry avatar Feb 05 '19 18:02 tayloraharry

I'm missing this feature. It really needs to be standard.

edumrodrigues avatar Feb 20 '19 11:02 edumrodrigues

found out why it's so hard to set a calendar mask. got to build a directive based on the inputMask to use on the pCalendar. One of the main problems to solve this problem is the DateFormat options, and the possible functionalities(range, multiple and whatsoever functionality). If set just for numeric values. the mask can be pretty simple to make.

shogogan avatar Feb 20 '19 16:02 shogogan

Many users find it impractical to have to select a date in the calendar, they want to type on the keyboard when using the mouse, and this is really true. I think it would be a question of usability to think (@cagataycivici), for now I have created a method to use the inputMask with the validation of the user input:

Component:

import { Component, OnInit, Input, forwardRef, ChangeDetectorRef, Output, EventEmitter, Directive } from '@angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor, NG_VALIDATORS, Validator, AbstractControl } from '@angular/forms';
import * as moment from 'moment';

export const LNDATE_VALUE_ACCESSOR: any = {
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => AppLnDateComponent),
  multi: true
};

@Component({
  selector: 'app-app-ln-date',
  templateUrl: './app-ln-date.component.html',
  styleUrls: ['./app-ln-date.component.css'],
  providers: [LNDATE_VALUE_ACCESSOR]
})
export class AppLnDateComponent implements OnInit, ControlValueAccessor {
  @Input()
  mask = '99/99/9999';
  @Input()
  inputMask = 'YYYY-MM-DD';
  @Input()
  displayMask = 'DD/MM/YYYY';

  @Output() onChange: EventEmitter<any> = new EventEmitter();
  @Input() disabled: boolean;
  @Input() url: boolean;
  onModelChange: Function = () => { };

  onModelTouched: Function = () => { };
  focused: boolean = false;
  value: string;
  valueAcessor: string;
  keySelected: number;

  completed = false;

  constructor(private cd: ChangeDetectorRef) { }

  ngOnInit() {
  }

  writeValue(obj: any): void {
    if (obj && !moment(obj, this.inputMask, true).isValid()) {
      throw new Error(`Data invΓ‘lida deve estar no formato correto, ${obj}`);
    }
    this.value = moment(obj).format(this.inputMask);
    this.valueAcessor = moment(this.value, this.inputMask).format(this.displayMask);
    this.cd.markForCheck();
  }

  registerOnChange(fn: any): void {
    this.onModelChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onModelTouched = fn;
  }

  setDisabledState?(val: boolean): void {
    this.disabled = val;
  }

  onFocus(event: Event) {
    this.focused = true;
  }

  onBlur(event: Event) {
    this.focused = false;
    if(!this.isValidDate()) {
     /* this.valueAcessor = moment(this.value, this.inputMask).format(this.displayMask);*/
     setTimeout(() => { 
     /*  this.valueAcessor = moment(this.value, this.inputMask).format(this.displayMask);*/
     this.valueAcessor = '';
     this.value = undefined;
     this.updateModel(event, undefined);
     }, 100);
    }
  }

  isValidDate() {
    return moment(this.valueAcessor, this.displayMask, true).isValid();
  }

  onIputKey(evt) {
    this.value = evt;
    this.updateModel(evt, String(this.value));
  }

  onInputChange() {
    //console.log('digito');

    // let val = (<HTMLInputElement>event.target).value;
    let setVal = moment(this.valueAcessor, this.displayMask).format(this.inputMask);
    if (setVal !== 'Invalid date') {
      this.updateModel(event, setVal);
    }
  }

  onInput(event: KeyboardEvent) {
    this.value = (<HTMLInputElement>event.target).value;
    this.onModelChange(this.value);
  }

  updateModel(event: Event, value: string) {
    this.value = value;
    this.onModelChange(this.value);
    this.onChange.emit({
      originalEvent: event,
      value: value
    });
  }


}

@Directive({
  selector: '[appCheckDate]',
  providers: [{ provide: NG_VALIDATORS, useExisting: DateValidatorDirective, multi: true }]
})
export class DateValidatorDirective implements Validator {
  @Input('dateValue') dateValue: string;
  @Input('dateFormat') dateFormat: string;

  validate(control: AbstractControl): { [key: string]: any } | null {
    return this.dateValue && moment(control.value, this.dateFormat, true).isValid() ?
      null : {
        invalidDate: {
          valid: false
        }
      };
  }
}

HTML:

<p-inputMask [mask]="mask" (onFocus)="onFocus($event)" (onBlur)="onBlur($event)" appCheckDate (onComplete)="onInputChange()" [dateValue]="valueAcessor" [(ngModel)]="valueAcessor" [dateFormat]="displayMask" placeholder="DD/MM/YYYYY"></p-inputMask>

IN FORM OR USE NGMODULE:

<app-app-ln-date mask="99/99/9999" inputMask="YYYY-MM-DD" displayMask="DD/MM/YYYY" formControlName="data_internacao"></app-app-ln-date>

obs: use moment.js to format and valid dates ...

leonetosoft avatar Feb 27 '19 07:02 leonetosoft

I've made one lib for masking, one of the first things that i built was a directive for primeng calendar. https://www.npmjs.com/package/racoon-mask

it's quite simple and solve most of the directive problems, if anyone find anything that could improve it, this is the github link :)

https://github.com/shogogan/racoon-mask

it has two directives, but the main one here is the [rPCalendarMask], no inputs, no outputs, it just gets the dateformat and builds a mask on it (with time included if wanted)

usage: HTML: <p-calendar rPCalendarMask></p-calendar>

shogogan avatar Feb 27 '19 17:02 shogogan

Just informing that i changed the mask lib name (racoon-lib to racoon-mask). And now it can show the placeholder like __/__/____

and it is working pretty better (with a early stage of a showcase to try on while testing)

And I put it on version 1.0.0 right now. πŸ˜„

still needs some improvements, but by now it's working on android/ios chrome/ie10

Any issue just report or open a PR :)

shogogan avatar Mar 03 '19 02:03 shogogan

Just informing that i changed the mask lib name (racoon-lib to racoon-mask). And now it can show the placeholder like __/__/____

and it is working pretty better (with a early stage of a showcase to try on while testing)

And I put it on version 1.0.0 right now.

still needs some improvements, but by now it's working on android/ios chrome/ie10

Any issue just report or open a PR :)

Thank you so much for your work. I'm using the rPCalendarMask directive but cannot get __/__/____ to show while the user is inputting the date.

As this sample shows for phone number, when the user selects the input, I would like for the __/__/____ to be in place of ___-___-____, but for p-calendar element.

image

I'm using racoon-mask-primeng version 1.1.11. It would be very helpful if you can create a demo of this.

Thank you again.

iwashungry1992 avatar May 13 '19 23:05 iwashungry1992

Hey @iwashungry1992, to show the placeholder, just set [showPlaceholder]="true" it should work correctly then, it will pick the dateformat informed on the p-calendar and set the placeholder accordingly.

If not then it may be a bug, and i need to fix it

shogogan avatar May 14 '19 01:05 shogogan

Hey @iwashungry1992, to show the placeholder, just set [showPlaceholder]="true" it should work correctly then, it will pick the dateformat informed on the p-calendar and set the placeholder accordingly.

If not then it may be a bug, and i need to fix it

This made it work, thank you!

iwashungry1992 avatar May 14 '19 14:05 iwashungry1992

@shogogan One thing I noticed that may be a bug - when the user deletes, they can only delete to the closest "/". Then they cannot delete anymore unless they move the mouse cursor manually into the next set of "/". Also, when this happens, the placement of the cursor isn't in the correct spot.

See below: image

iwashungry1992 avatar May 14 '19 15:05 iwashungry1992

Just another workaround suggestion...

It seems it is possible to use Cleave alongside the Calendar component.

After

npm i --save cleave.js @types/cleave.js

you can configure your component something like this:

import Cleave from 'cleave.js';
import { Calendar } from 'primeng/calendar';

@ViewChild('calendarDataInicio')
calendarDataInicio: Calendar;

cleaveDataInicio: Cleave;

ngAfterViewInit() {
  this.cleaveDataInicio = new Cleave(this.calendarDataInicio.inputfieldViewChild.nativeElement,
    {
      date: true,
      datePattern: ['d', 'm', 'Y'],
    }
  );
}

ligiane avatar Jun 10 '19 16:06 ligiane

Any official solution? Or official response from primeng?

jsilveira2 avatar Aug 01 '19 19:08 jsilveira2