2
votes

I have updated my Angular2 RC5 application to RC6. I have developed some custom form controls based on this tutorial from Thoughtram.

Everything was working until RC5, however after update the validation is not working anymore after a bit of investigation I found that the control's value is not reflected in the associated model.

You can find the original plunker from Thoughtram's tutorial here.

To reproduce the issue update the version information in systemjs.config.js file from

var ngVer = '@2.0.0-rc.5'; 
var routerVer = '@3.0.0-rc.1'; 
var formsVer = '@0.3.0'; 
var routerDeprecatedVer = '@2.0.0-rc.2'; 

to

var ngVer = '@2.0.0-rc.6'; 
var routerVer = '@3.0.0-rc.2'; 
var formsVer = '@2.0.0-rc.6';

After the version update you will see that the control value is not updated and due to this the validation does not work.

However, if I update the angular version to @2.0.0-rc.6 and keep the forms version intact i.e. @0.3.0, it works.

UPDATE 1: Code for the component is

import { Component, OnInit, forwardRef, Input, OnChanges } from '@angular/core';
import { FormControl, ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';


export function createCounterRangeValidator(maxValue, minValue) {
  return (c: FormControl) => {
    let err = {
      rangeError: {
        given: c.value,
        max: maxValue || 10,
        min: minValue || 0
      }
    };

  return (c.value > +maxValue || c.value < +minValue) ? err: null;
  }
}

@Component({
  selector: 'counter-input',
  template: `
    <button (click)="increase()">+</button> {{counterValue}} <button (click)="decrease()">-</button>
  `,
  providers: [
    { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => CounterInputComponent), multi: true },
    { provide: NG_VALIDATORS, useExisting: forwardRef(() => CounterInputComponent), multi: true }
  ]
})
export class CounterInputComponent implements ControlValueAccessor, OnChanges {

  propagateChange:any = () => {};
  validateFn:any = () => {};

  @Input('counterValue') _counterValue = 0;
  @Input() counterRangeMax;
  @Input() counterRangeMin;

  get counterValue() {
    return this._counterValue;
  }

  set counterValue(val) {
    this._counterValue = val;
    this.propagateChange(val);
  }

  ngOnChanges(inputs) {
    if (inputs.counterRangeMax || inputs.counterRangeMin) {
      this.validateFn = createCounterRangeValidator(this.counterRangeMax, this.counterRangeMin);
    }
  }

  writeValue(value) {
    if (value) {
      this.counterValue = value;
    }
  }

  registerOnChange(fn) {
    this.propagateChange = fn;
  }

  registerOnTouched() {}

  increase() {
    this.counterValue++;
  }

  decrease() {
    this.counterValue--;
  }

  validate(c: FormControl) {
    return this.validateFn(c);
  }
}

Main module looks like:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { CounterInputComponent } from './counter-input.component.ts';

@NgModule({
  imports: [BrowserModule, FormsModule, ReactiveFormsModule],
  declarations: [AppComponent, CounterInputComponent],
  bootstrap: [AppComponent]
})
export class AppModule {}

and I am using the component in my app.component like

import { Component } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { createCounterRangeValidator } from './counter-input.component';

@Component({
  selector: 'my-app',
  template: `
    <h2>Inside Form</h2>
    <div>
      <label>Change min value:</label>
      <input [(ngModel)]="minValue">
    </div>
    <div>
      <label>Change max value:</label>
      <input [(ngModel)]="maxValue">
    </div>
    <form [formGroup]="form">
      <p>Control value: {{form.controls.counter.value}}</p>
      <p>Min Value: {{minValue}}</p>
      <p>Max Value: {{maxValue}}</p>
      <p>Form Value:</p>
      <pre>{{ form.value | json }}</pre>

      <counter-input
        formControlName="counter"
        [counterRangeMax]="maxValue"
        [counterRangeMin]="minValue"
        [counterValue]="50"
        ></counter-input>
    </form>

    <p *ngIf="!form.valid">Form is invalid!</p>


    <h2>Standalone</h2>
    <counter-input
      counterMinValue="0"
      counterMaxValue="3"
      [counterValue]="counterValue"></counter-input>
  `
})
export class AppComponent {

  form:FormGroup;
  counterValue = 3;
  minValue = 0;
  maxValue = 12;

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this.form = this.fb.group({
      counter: this.counterValue
    });
  }

}

Update 2: I've reported this issue on Github here

2

2 Answers

0
votes

Thanks for updating your source. I have a control that does essentially what you're doing here also:

This is the child component spinner:

@Component({
  selector: 'kg-spinner',
  templateUrl: './app/shared/numberSpinner/kgSpinner.component.html',
  styleUrls: ['./app/shared/numberSpinner/kgSpinner.component.css']
})

export class KgSpinnerComponent implements OnInit {
  @Input('startValue') curValue: number;
  @Input() range: number[];
  @Input() increment: number;
  @Input() spinName;
  @Input() precision: number;
  @Input() theme: string;

  @Output() onChanged = new EventEmitter<SpinnerReturn>();

  lowerLimit: number;
  upperLimit: number;
  name: string;
  curTheme: Theme;
  errorMessage: string;
  sr: SpinnerReturn;
  appPageHeaderDivStyle: {};
  unit: string = "(g)";

  constructor(private ts: ThemeService) {
    this.sr = new SpinnerReturn();
  }

  ngOnInit() {
    this.lowerLimit = this.range[0];
    this.upperLimit = this.range[1];
    this.appPageHeaderDivStyle = this.ts.getAppPageHeaderDivStyle();
    if(this.spinName === "carbGoal") {
      this.unit = "(g)";
    } else if (this.spinName === "proteinGoal") {
      this.unit = "(g)";
    } else {
      this.unit = "(%)";
    }
  }

The html:

<div class="ui-grid-col-8 spinnerMargin">
                      <kg-spinner spinName="proteinGoal" [range]=[.6,1.2] [increment]=.1 [startValue]=.6 [precision]=1 (onChanged)="onChanged($event)"></kg-spinner>
                    </div>

The parent component onChanged:

  onChanged(sr: SpinnerReturn) {
        if (sr.spinName === "carbGoal") {
            (<FormControl>this.macroForm.controls['carbGoal']).setValue(sr.spinValue);
        } else if (sr.spinName === "proteinGoal") {
            (<FormControl>this.macroForm.controls['proteinGoal']).setValue(sr.spinValue);
        } else if (sr.spinName === "calorieDifference") {
            (<FormControl>this.macroForm.controls['calorieDifference']).setValue(sr.spinValue);
        }

This all works perfectly on RC6. Hope this helps you solve your problem.

0
votes

An optional registerOnChange() function was introduced in RC.6 for validator directives and there already exists a function with same name in controlValueAccessor which caused a conflicts and registerOnChange in my component was invoked twice.

This has been fixed under the issue.

Suggested temporary workaround is

@Component({
  ...
  ...
})
export class CounterInputComponent implements ControlValueAccessor, OnChanges {

  isPropagate: boolean = false;

  /*Rest of the class implementation
  ...
  ...
  */

  registerOnChange(fn) {
    if (this.isPropagate) {
      return;
    }

    this.propagateChange = fn;
    this.isPropagate = true;
  }

  //.....
}