Ana_30
Ana_30

Reputation: 375

Quarterly datepicker

I am looking for a Quarterly date picker using ng-bootstrap.

At the moment I have Month and Year see STACKBLITZ but I would like to change the Month to Quarter.

enter image description here

Is this possible with ng-bootstrap?

For info: here is a previous stackblitz example using Angular Material

enter image description here

Upvotes: 0

Views: 6303

Answers (1)

Eliseo
Eliseo

Reputation: 57939

"briefly" explain about the stackblitz

Basicaly we has a ng-dropDown that show a series of ngbDropdownItem

we has as variables

year:number; //the year selected
quarter:string; //hte quarter selected

yearDefault=new Date().getFullYear() //the year by defect
quarterDefault="Q"+(1+Math.floor(new Date().getMonth()/3)) //the quarter by defect

showQuarter:boolean=true; //a boolean variable. 
         //if true, the ngbDropdownItems will be the quarter, 
         //else the years

year10:number; //auxliar for show the list of years

And an auxiliar array to show the quarter

options: any[] = [
    {value:'Q1',months:['Jan','Feb','Mar']},
    {value:'Q2',months:['Apr','May','Jun']},
    {value:'Q3',months:['Jul','Aug','Sep']},
    {value:'Q4',months:['Oct','Nov','Dec']}
  ];

So, in our ngbDropdownMenu we can show

<ng-container *ngIf="showQuarter">
    <button [ngClass]="{'bg-primary':item.value==quarter}" ngbDropdownItem 
         *ngFor="let item of options" 
         (click)="click(item.value,drop)">
        <span class="col" *ngFor="let month of item.months" >
           {{month}}
        </span>
    </button>
</ng-container>

//or

<ng-container *ngIf="!showQuarter">
    <button [ngClass]="{'bg-primary':(year10+item)==year}" ngbDropdownItem 
          *ngFor="let item of [0,1,2,3,4,5,6,7,8,9]" 
          (click)="changeYear(year10+item);showQuarter=true">
        <span >{{year10+item}}</span>
    </button>
</ng-container>

Moreover, we show a "header" with two buttons (left and rigth arrow) and a span that show or the year or the decade

<div class="selectYear">
    <div class="ngb-dp-arrow">
        <button  class="btn btn-link ngb-dp-arrow-btn" type="button"
            (click)="showQuarter?changeYear((year||yearDefault)-1):year10=year10-10">
           <span class="ngb-dp-navigation-chevron">
            </span>
        </button>
    </div>
    
    <button type="button" class="btn btn-link" (click)="changeShowQuarter()">
        {{showQuarter?year?year:yearDefault:(year10+' - '+(year10+9))}}
    </button>
    
    <div class="ngb-dp-arrow right">
        <button class="btn btn-link ngb-dp-arrow-btn" type="button" 
            (click)="showQuarter?changeYear((year||yearDefault)+1):year10=year10+10">
            <span class="ngb-dp-navigation-chevron">
            </span>
        </button>
    </div>
</div>

See how, depending the variable "showQuarter" the buttons make one or another action

The functions are simples, again we make take account that at first, year and quarter can has no value, in that case we use yearDefault and QuarterDefault

changeYear(year)
  {
    this.year=year || this.yearDefault;
    this.quarter=this.quarter||this.quarterDefault
              this.control.setValue(this.quarter+" "+this.year || this.yearDefault,{emitEvent:false})
  }
  changeShowQuarter()
  {
    this.showQuarter=!this.showQuarter
    if (!this.showQuarter)
      this.year10=this.year?10*Math.floor(this.year/10):10*Math.floor(this.yearDefault/10)
  }
  click(quarter,drop)
  {
    this.quarter=quarter;
    this.year=this.year||this.yearDefault
              this.control.setValue(this.quarter+" "+this.year,{emitEvent:false})
    drop.close()
  }

And yes, we has a FormControl called control, becaouse we has an input

  <input style="text-transform: uppercase" [formControl]="control" placeholder="Qq yyyy" >
  
  control:FormControl= new FormControl()

To control the manually entry of the quarter and year we subscribe to control.valueChanges, to give value to year and quarter only if the length of string is greater or equal to 6

this.control.valueChanges.pipe(
      takeWhile(()=>this.alive),
      startWith(this.quarter+" "+this.year),
      debounceTime(200))
    .subscribe((res:string)=>{
//      console.log(this.controlID.nativeElement.selectionStart)
       if (res)
       {
         res=res.toUpperCase()
         if (res[0]!="Q")
           res="Q"+res;
           let value=res.replace(/[^Q|0-9]/g, '');
           let quarter;
           let year;
           if (value.length>=2)
              quarter=value[0]+value[1]
           if (value.length>=6)
           {
              year=value.substr(2,4)
              this.year=+year
              this.quarter=quarter;
              this.control.setValue((this.quarter+" "+this.year),{emitEvent:false})
           }
           else
           {
             this.year=null;
             this.quarter=null;
           }
       }
    })

TODO: create a custom form control with the component, revised the .css to improve and remove unnecesary styles

Update convert in a CustomFormControl it's easy, only implements ControlValueAccessor

  disabled: boolean = false;
  onChange: (_: any) => void;
  onTouched: any;

  registerOnChange(fn: (_: any) => void): void {
    this.onChange = fn;
  }

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

  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }
  writeValue(value: any): void {
    if (value) {
      this.year = value.year;
      this.quarter = value.quarter;
      if (this.year && this.quarter)
        this.control.setValue(this.quarter + " " + this.year,{emitEvent:false});
    }
  }

I use an auxiliar function

  setValue(data: any) {
      if (data && data.quarter && data.year) {
        this.control.setValue(data.quarter + " " + data.year, {
          emitEvent: false
        });
        this.onChange({ quarter: data.quarter, year: data.year });
      } else {
        this.control.setValue(data, { emitEvent: false });
        this.onChange(null);
      }
  }

That call in control.valueChanges subscribe and in click function

I leave in this stackblitz

NOTE: like ng-bootstrap I choose that the return value was an object with the properties year and quarter

Update @Mohan ask about improve the month date picker adding a minQuarter.

To add a minQuarter and maxQuarter it's "only" add two inputs more

@Input() minQuarter={ quarter: "Q1", year: 0 };
@Input() maxQuarter={ quarter: "Q4", year: 9999 }

Well, now we need disabled the buttons to take account this values. There are severals buttons and we need take caraful when disabled it

//arrow left
[disabled]="minQuarter.year>=(showQuarter?year:year10)"

//arrow right
[disabled]="maxQuarter.year<=(showQuarter?year:(year10+10))" 

//the quarters
[disabled]="(minQuarter.year==year && item.value<minQuarter.quarter) 
          ||(maxQuarter.year==year && item.value>minQuarter.quarter)"

//the years
[disabled]="((year10+item)<minQuarter.year || (year10+item)>maxQuarter.year)"

more over, we can take accont when we change manually the quarter, force to get the minQuarter or maxQuarter. So in the subscribe to control.valueChanges,

this.control.valueChanges
  .pipe(...)
  .subscribe((res: string) => {
    let quarter = null;
    let year = null;
        ...
    //here check the min and max value
    if (year)
    {
       if (year<this.minQuarter.year)
           year=this.minQuarter.year

       if (year>this.minQuarter.year)
           year=this.maxQuarter.year

       if (year==this.minQuarter.year && quarter<this.minQuarter.quarter)
           quarter=this.minQuarter.quarter;

       if (year==this.maxQuarter.year && quarter>this.minQuarter.quarter)
           quarter=this.maxQuarter.quarter;
    }
    ....
  });

}

If I not missed, it's all. I updated the stackblitz

Upvotes: 2

Related Questions