2

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 Answers 2

0

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.

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

7 Comments

As you can see I am not using any deprecated thing in the source code provided. So its some other issue I believe.
There is a difference here. My component is a custom form control which can make a form valid/invalid based on the value. In your case you are writing a custom component. The problem I am facing only appears if you write a custom form control. The link to the custom form controls is blog.thoughtram.io/angular/2016/07/27/…
Okay... but I don't see the differentiation between the two. I only allow valid values in the child component, so their output is always valid.
I really appreciate your effort to solve my issue. But as I said earlier that my component is a form control which means you can put it inside a form and set builtin/custom Validators on it and If the validation will fail the form will become invalid. If you will read the link I sent in my earlier comment above, you will get the whole picture.
You're missing my point. If you are implementing a spinner provide it valid ranges like I did in my sample. I use this in a number of forms and it works perfectly. I only allow it valid ranges for input and set a valid default. Form always stays valid because these controls are always valid. I don't see the point in allowing users to enter non-valid data...
|
0

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;
  }

  //.....
}

Comments

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.