Why ngModel don't trigger changedetection in writevalue method? - javascript

I wrote a very simple custom form control and I didn't change it's changeDetectionStrategy.
#Component({
selector: 'counter',
template: `
<button (click)="increase($event)">+</button>
{{counter}}
<button (click)="decrease($event)">-</button>
`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CounterComponent),
multi: true
}]
})
export class CounterComponent implements OnInit, ControlValueAccessor {
private counter: number = 0;
private onChange: (_: any) => void;
private onTouched: () => void;
constructor(private _cdr: ChangeDetectorRef) { }
ngOnInit() { }
writeValue(value) {
console.log(`Write value`, value);
this.counter = value;
// this._cdr.markForCheck(); // it works
// Use onChange works too
// if (this.onChange) {
// this.onChange(value);
// }
}
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
increase() {
this.counter++;
this.onChange(this.counter);
}
decrease() {
this.counter--;
this.onChange(this.counter);
}
}
Then I use it in a component named ngmodel-demo with onPush changeDetectionStrategy.
#Component({
selector: 'ngmodel-demo',
template: `
<h3>NgModel Demo</h3>
<p>Count: {{count}}</p>
<counter [(ngModel)]="count"></counter>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NgmodelDemoComponent {
#Input() name: string;
public count = 1;
constructor(private _cdRef: ChangeDetectorRef) {}
}
When I ran the app, I found that the counter component have got the value 1, but its view didn't update.
Then I set a timer to update the ngModel and mark for check.
ngOnInit() {
setInterval(() => {
this.count = ++this.count;
this._cdRef.markForCheck();
}, 3000);
}
The result is that each time the value that counter component's view shows is the value of the last ngModel's.
Manually calling markForCheck in writeValue method works. But I did not use the onPush strategy, I do not understand why to manually call?
There is also a puzzle that is why calling onChange in writeValue also works.
Online demo link on stackblitz: https://stackblitz.com/edit/angular-cfc-writevalue

It is a bug in Angular. I opened an issue about it. You can subscribe if interested.

Related

Angular HTML Binding is not working when we removed HostListener

to make change detection lesser, we replace hostlistener with from event from RXJS and runoutside of angular.
this is how my angular code looks like
ngOnInit() {
this.windowKeyDown();
// this.subject$ = this.subject.asObservable();
}
constructor(private _ngZone: NgZone) {}
//#HostListener('window:keydown', ['$event'])
handleKeyboardEvent(event: KeyboardEvent) {
console.log('handle key fired');
this.keypressed = event.key;
this.iskeyPressed = true;
}
windowKeyDown() {
console.log('windowKeyDown');
fromEvent(window, 'keydown')
.pipe(this.outsideZone(this._ngZone))
.subscribe(event => this.handleKeyboardEvent(<KeyboardEvent>event));
}
outsideZone<T>(zone: NgZone) {
return function(source: Observable<T>) {
return new Observable(observer => {
let sub: Subscription;
zone.runOutsideAngular(() => {
sub = source.subscribe(observer);
});
return sub;
});
};
}
and HTML binding is :
<h2>keypressed: {{keypressed}}</h2>
<h2>iskeyPressed: {{iskeyPressed}}</h2>
in this binding variable it self not updating now, can you please guide what's wrong with my code?
minimum step to repro : https://stackblitz.com/edit/keypress-example-vu3mej?file=app%2Fapp.component.html
I would recommend setting the ChangeDetectionStrategy for this component to OnPush. You can read more about it here. Do not put any logic in this component for which you want automatic change detection as changedetection is disabled for the component as a whole.
Below is a code sample that shows how you can subscribe to the keyboard events directly from the template (using the async pipe).
https://stackblitz.com/edit/angular-change-detection-strategy-onpush-g6mjkr?file=src%2Fapp%2Fchild%2Fchild.component.ts
import {
Component,
Input,
ChangeDetectionStrategy,
OnInit
} from '#angular/core';
import { fromEvent, Observable } from 'rxjs';
import { map, startWith, tap } from 'rxjs/operators';
#Component({
selector: 'child',
template: `
<ng-container *ngIf="keyboard$ | async as keyBoard">
<h2>keypressed: {{ keyBoard.keypressed }}</h2>
<h2>iskeyPressed: {{ keyBoard.iskeyPressed }}</h2>
</ng-container>
`,
styleUrls: [],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChildComponent implements OnInit {
keyboard$: Observable<{ keypressed: any; iskeyPressed: boolean }>;
ngOnInit() {
console.log('bla')
this.keyboard$ = fromEvent(window, 'keydown').pipe(
map(event => ({ keypressed: event.key, iskeyPressed: true })),
startWith({keypressed: null, iskeyPressed: null})
);
}
}

Angular: Accessor not doing two-way-binding

I am trying to write an accessor component, for an input that has a label associated with it, but I am not sure if I am doing this correctly. When I attempt to do two-way-binding, the value isn't set in the input field even though it is set within the class MyComponent.
Why isn't my two-way-binding working? I would expect to see Something Special inside the <input> that is within <third-party-input>. However, the input is empty.
When using <third-party-input> on its own, two-way-binding works there, just not when I wrap my component around it.
Main Component
<ui-input [(ngModel)]="myValue"></ui-input>
export class MyComponent {
public myValue = 'Something Special';
}
Input With Label Component
<label>{{label}}</label>
<third-party-input [(ngModel)]="value"></third-party-input>
#Component({
selector: 'ui-input',
templateUrl: './input.component.html',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputComponent),
multi: true
}]
})
export class InputComponent extends AccessorComponent {}
#Component({
selector: 'app-accessor',
template: ''
})
export abstract class AccessorComponent implements ControlValueAccessor {
protected _value: any;
public get value() {
return this._value;
}
public set value(val) {
this._value = val;
this.onChange(val);
this.onTouched();
}
protected onChange = (_: any) => { };
protected onTouched: any = () => { };
public registerOnChange(fn: (_: any) => void): void {
this.onChange = fn;
}
public registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
public writeValue(value: any): void {
if (value !== undefined) {
this._value = value;
}
}
}

Angular - Why does clicking on a checkbox fire a change event for the parent selector in which the change event is bound to?

So, I was just minding my own business on Gitter when I came across something interesting. Take a look at this Stackblitz demo.
It uses a custom directive that makes a custom checkbox component work with ngModel/formControl etc.
What I don't understand here is how the events set for host in the NgCheckboxControlDirective work. Since the host here is the selector for the checkbox component and not the checkbox input itself, it shouldn't trigger any change or blur event.. but it does.
If you click on the checkbox in the demo linked at the top you'll see that choosen changes between true and false.
Why? Why does clicking on the checkbox fire a change event here which then updates the choosen state?
The directive which implements ControlValueAccessor:
import { Directive, Renderer2, ElementRef } from '#angular/core';
import { ControlValueAccessor , NG_VALUE_ACCESSOR } from '#angular/forms';
#Directive({
selector: '[ngCheckboxControl]',
host: {'(change)': 'onChange($event.target.checked)', '(blur)': 'onTouched()'},
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: NgCheckboxControlDirective,
multi: true
}
]
})
export class NgCheckboxControlDirective implements ControlValueAccessor {
onChange = (_: any) => {};
onTouched = () => {};
constructor(private _renderer: Renderer2, private _elementRef: ElementRef) {}
writeValue(value: any): void {
this._renderer.setProperty(this._elementRef.nativeElement.firstElementChild, 'checked', value);
}
registerOnChange(fn: (_: any) => {}): void { this.onChange = fn; }
registerOnTouched(fn: () => {}): void { this.onTouched = fn; }
setDisabledState(isDisabled: boolean): void {
this._renderer.setProperty(this._elementRef.nativeElement.firstElementChild, 'disabled', isDisabled);
}
}
The checkbox:
import { Component, ElementRef } from '#angular/core';
import { NgControl } from '#angular/forms';
#Component({
selector: 'checkbox-layout',
template: `
My checkbox: <input type="checkbox" />
`,
})
export class CkeckboxLayoutComponent {
constructor(
private ngControl: NgControl,
private elementRef: ElementRef
) {
console.log('... elementRef:', elementRef);
console.log('... ngControl:', ngControl);
}
}
Then you'd use it like this:
<checkbox-layout formControlName="choosen" ngCheckboxControl></checkbox-layout>

Angular components as self validating formControls

I'm not sure if there's a better way to do this, I assume there should be. Basically I have a component I want to treat as a standalone form control. This control will always have some sort of special validation attached and I would like it to bubble up to the form whenever the component is used.
I've attached a plunker. Is there a way to for the form to be marked invalid if the component/formControl is invalid? I know I could add the validator to the form itself, but I would like to make things easy and more predictable for future use of this component. I'm also open to better ideas of doing this.
#Component({
selector: 'my-app',
template: `
<form [formGroup]="form">
<my-component-control formControlName="myComponent"></my-component-control>
</form>
<div>Form Value: {{form.value | json}}</div>
<div>Form Valid: {{form.valid}}</div>
`,
})
export class App {
constructor(fb: FormBuilder) {
this.form = fb.group({
myComponent: ''
});
}
}
#Component({
selector: 'my-component-control',
template: `
<div>Control Errors: {{control.errors | json}}</div>
<input [formControl]="control">
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => MyComponentComponent),
}
]
})
export class MyComponentComponent implements ControlValueAccessor {
control: FormControl;
onChange: any = () => { };
onTouched: any = () => { };
constructor() {
this.control = new FormControl('', (control) => {
return control.value ? null : {shouldHaveSomething: true};
});
this.control.valueChanges.subscribe((value) => {
this.onChange(value);
this.onTouched();
});
}
writeValue (obj: any): void {
this.control.setValue(obj);
}
registerOnChange (fn: any): void {
this.onChange = fn;
}
registerOnTouched (fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
throw new Error('Method not implemented.');
}
}
One solution could be using setValidators method on host AbstractControl
To do so I'm going to get reference to AbstractControl through NgControl. We can't just inject NgControl in constructor because we'll get issue with instantiating cyclic dependency.
constructor(private injector: Injector) {
...
}
ngOnInit() {
Promise.resolve().then(() => {
const hostControl = this.injector.get(NgControl, null);
if (hostControl) {
hostControl.control.setValidators(this.control.validator);
hostControl.control.updateValueAndValidity();
}
});
}
Ng-run Example

Why Pipe doesn't work correctly in Angular2?

The task is simple, it is necessary that the input was entered only numbers below a certain number. I did so:
export class MaxNumber implements PipeTransform{
transform(value, [maxNumber]) {
value = value.replace(/[^\d]+/g,'');
value = value > maxNumber?maxNumber:value;
return value;
}
}
and then in the template called something like:
<input type="text" [ngModel]="obj.count | maxNumber:1000" (ngModelChange)="obj.count=$event" />
But it works very strange click.
I probably misunderstand something. I would be grateful if someone will explain that behavior.
I think that you need rather a custom value accessor. This way you will be able to check the value before setting it in the ngModel. This way you obj.count won't be upper than 1000.
Here is a sample implementation:
const CUSTOM_VALUE_ACCESSOR = new Provider(
NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => MaxNumberAccessor), multi: true});
#Directive({
selector: 'input',
host: {'(input)': 'customOnChange($event.target.value)'},
providers: [ CUSTOM_VALUE_ACCESSOR ]
})
export class MaxNumberAccessor implements ControlValueAccessor {
onChange = (_) => {};
onTouched = () => {};
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
writeValue(value: any): void {
var normalizedValue = (value == null) ? '' : value;
this._renderer.setElementProperty(this._elementRef.nativeElement, 'value', normalizedValue);
}
customOnChange(val) {
var maxNumber = 1000;
val = val.replace(/[^\d]+/g,'');
val = val > maxNumber?maxNumber:val;
this.onChange(val);
}
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}
There is nothing to do in your component to use it than setting this directive into its directives attribute:
#Component({
selector: 'my-app',
template: `
<div>
<h2>Hello {{name}}</h2>
Number: <input type="text" [(ngModel)]="obj.count" />
<p>Actual model value: {{obj.count}}</p>
</div>
`,
directives: [MaxNumberAccessor]
})
export class App {
(...)
}
Corresponding plunkr: https://plnkr.co/edit/7e87xZoEHnnm82OYP84o?p=preview.

Categories