简体   繁体   中英

Angular2+ & NgbDatePicker: proper ControlValueAccessor for Date ngModel

Was working on some custom wrappers to the ng-bootstrap . The idea of the one in question is to create a component that takes in a Date as its [ngModel].

I noticed a bug with my wrapper implementing it in an app I'm working on that required multiple components of the same object but with different references on the same page. Seems that if I create 2 variables that should reference the same object, they actually get out of sync with each other. Eg:

this.date: Date = new Date();
this.copy: Date = date;
....
<my-component [ngModel]="date"></my-component>
<my-component [ngModel]="copy"></my-component>
<-- Components don't stay in sync - date and copy point at different Dates -->

I understand that this is because my implementation of the ControlValueAccessor is faulty. Somehow I'm overwriting the reference to point to a new Date object instead of modifying the given object.

I believe this is caused by line 56 on my plunker? But may also have something to do with ControlValueAccessor that I'm not doing/understanding correctly. Seems too like adjusting my Date object to match the NgbDateStruct of the wrapped NgbDatePicker via of the DoCheck lifecycle hook is a bit hacky (though I don't think it's causing the issue - would much prefer a clever RxJS solution, but haven't been able to think of one yet.)

Plunker link

my app code:

@Component({
selector: 'my-app',
template: `
  <h1>
    {{title}}
  </h1>
  <hr>
  <h2>Day Picker</h2>
  <app-day-picker [(ngModel)]="dayPickerDay"></app-day-picker>
  Day: {{ dayPickerDay | date:mediumDate }}
  <br>
  <button (click)="setToYesterday()">Change to yesterday</button>
  <button (click)="setToTomorrow()">Change to tomorrow</button>
  <hr>
  <h2>Day Picker for copy (should stay in sync... doesn't.</h2>
  <app-day-picker [(ngModel)]="dayPickerCopy"></app-day-picker>
  Day: {{ dayPickerCopy | date:mediumDate }}
  <br>`
})
export class App {
  title = 'My time components';
  dayPickerDay: Date;
  dayPickerCopy: Date;

  ngOnInit(){
    this.dayPickerDay = new Date(Date.now());
    this.dayPickerCopy = this.dayPickerDay;
  }

  setToTomorrow(){
    this.dayPickerDay = new Date(Date.now() + 24*1000*60*60);
  }

  setToYesterday(){
    this.dayPickerDay = new Date(Date.now() - 24*1000*60*60);
  }
}

My wrapper's code:

const noop = () => {};

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

@Component({
  selector: 'app-day-picker',
  template: `
<form class="form-inline">
  <div class="form-group">
    <div class="input-group">
      <input class="form-control" placeholder="Select a Date..."
       name="dp" [(ngModel)]="dayObject" ngbDatepicker #d="ngbDatepicker" disabled="">
      <button class="input-group-addon" (click)="d.toggle()" type="button">
        <i class="fa fa-calendar" style="width: 1.2rem; height: 1rem; cursor: pointer;"></i>
      </button>
    </div>
  </div>
</form>
  `,
  providers: [
    DAY_PICKER_CONTROL_VALUE_ACCESSOR
  ]
})
export class DayPickerComponent implements ControlValueAccessor, DoCheck {
  private innerValue: Date = new Date(Date.now());
  private dayObject: {year: number, month: number, day: number};

  private onTouchedCallback: () => void = noop;
  private onChangedCallback: (_: any) => void = noop;

  get value(): Date {
    return this.innerValue;
  }

  set value(v: Date){
    if(v.getTime() !== this.innerValue.getTime()){
      this.innerValue = v;
      let years = this.innerValue.getFullYear();
      let months = this.innerValue.getMonth() + 1;
      let days = this.innerValue.getDate();
      this.dayObject = {year: years, month: months, day: days};
      this.onChangedCallback(v);
    }
  }

  writeValue(value: Date){
    if(value === null) {
      value = new Date(Date.now());
    }
    if(value.getTime() !== this.innerValue.getTime()){
      this.innerValue = value;
      let years = this.innerValue.getFullYear();
      let months = this.innerValue.getMonth() + 1;
      let days = this.innerValue.getDate();
      this.dayObject = {year: years, month: months, day: days};
    }
  }

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

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

  ngDoCheck() {
    if (this.dayObject) {
      this.value.setFullYear(this.dayObject.year, this.dayObject.month - 1, this.dayObject.day);
    }
  }
}

The problem lies in this code:

writeValue(value: Date){
    if(value === null) {
        value = new Date(Date.now());
    }
    if(value.getTime() !== this.innerValue.getTime()){
        this.innerValue = value; //<-- Right here
        let years = this.innerValue.getFullYear();
        let months = this.innerValue.getMonth() + 1;
        let days = this.innerValue.getDate();
        this.dayObject = {year: years, month: months, day: days};
    }
}

You are assigning this.innervalue to the exact object that is being passed in by reference! Therefore when you do if(v.getTime() !== this.innerValue.getTime()) it will always be equal since it is the exact same Date object. To rectify all you need to do is:

this.innerValue = new Date(value);

to create a copy.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM