2

I created a custom input component for times. I can successfully set the initial value received by [(ngModel)], however any changes to the input field are not being processed. To be more precise: writeValue gets called initially, the setter does not.

The component looks as follows:

@Component({
  selector: 'app-timepicker',
  templateUrl: './timepicker.component.html',
  styleUrls: ['./timepicker.component.scss'],
  providers: [{provide: MatFormFieldControl, useExisting: forwardRef(() => NexupTimepickerComponent)}]
})
export class NexupTimepickerComponent implements OnDestroy, ControlValueAccessor {

  autofilled: boolean;

  // Subject for the changes on the input
  stateChanges = new Subject<void>();

  // Grouping of the input fields
  parts: FormGroup;

  public _onChange: any;

  constructor(fb: FormBuilder, private fm: FocusMonitor, private elRef: ElementRef<HTMLElement>,
              @Optional() @Self() public ngControl: NgControl
  ) {
    this.parts =  fb.group({
      'hour': '',
      'minute': ''
    });

    // control focus
    fm.monitor(elRef.nativeElement, true).subscribe(origin => {
      this.focused = !!origin;
      this.stateChanges.next();
    });

    this._onChange = (_: any) => {
    };
    if (this.ngControl) {
      this.ngControl.valueAccessor = this;
    }
  }

  // value
  @Input()
  get value(): Time | null {
    console.log('Getting value');
    const n = this.parts.value;
    if (n.hour.length === 2 && n.minute.length === 2) {
      return new Time(n.hour, n.minute);
    }
    return null;
  }

  set value(time: Time | null) {
    console.log('Setting value to: ', time);
    time = time || new Time('', '');
    this.parts.setValue({hour: time.hour, minute: time.minute});
    this.stateChanges.next();
  }

  ngOnDestroy() {
    this.stateChanges.complete();
    this.fm.stopMonitoring(this.elRef.nativeElement);
  }

  registerOnChange(fn: any): void {
    console.log('registered');
    this._onChange = fn;
  }

  registerOnTouched(fn: any): void {
  }

  setDisabledState(isDisabled: boolean): void {
  }

  writeValue(value: any): void {
    console.log('received value: ', value);
    if ((value !== this.parts.getRawValue()) && value) {
      console.log('writing value to: ', value);
      this.parts.setValue({hour: value.hour, minute: value.minute});
      console.log('new value: ', this.parts.getRawValue());
    }
  }

}

The HTML in the parent component is this:

<mat-form-field appearance="outline">
          <mat-label>End Time</mat-label>
          <app-timepicker [required]="true"
            [(ngModel)]="endTimeObject" #endTime="ngModel" name="endTime" ngDefaultControl>
          </app-timepicker>
</mat-form-field>

1 Answer 1

1

If the setter: set value is not called it's normally cause you don't have change the "value" value, you should have done somethine like this.value = "...";

To do this and for more control I generally add a [formControl]="myControl" on the inner input of my component (if I have one :)) and do something like this in the component :

myControl = new FormControl();

this.myControl.onStatusChange.subscribe( (newValue) => {
  this.value = newValue;
});

Futhermore it will be normal that [(ngModel)] will be not update to date cause you're never calling the this._onChange() callback.

The registerOnChage method give you a callback that you must call when you are updating the value from inside the component to notify the ngModel/formControl. I supposed you should add in set value :

 set value(time: Time | null) {
    console.log('Setting value to: ', time);
    time = time || new Time('', '');
    const newValue = {hour: time.hour, minute: time.minute};
    this.parts.setValue(newValue );
    this._onChange(newValue); // call the callback :)
    this.stateChanges.next();
  }

For more information here you can find a default implementation of the controlValueAccessor. You could extend this class in every component working with ngModel and simple change the value (this.value = 'newValue') and also override the writeValue cause it generally need to change to feet your component needs. https://github.com/xrobert35/asi-ngtools/blob/master/src/components/common/default-control-value-accessor.ts

Sign up to request clarification or add additional context in comments.

5 Comments

But if that was the case, shouldn't I see the console.log even though not calling this._onChange?
the set value will be called you somewhere in your code you will do "this.value = ...;" I can't see this in your code
That is right, sorry if I was unclear. "Setting value to" is not being logged in the console.
I thought registering the change function would detect changes. Where would I set the value?
I had to fix a bit more than that to get it working, especially set the ngModelChange. Nevertheless you're idea showed me the right direction, so thank you.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.