Angular2+: how does NgModel / NgControl internally handle updates from view to model? - javascript

I am doing a deep dive into how two-way databinding works. I am currently puzzled by how updates from the view (say, an input element) propagate to NgControl internally.
In the definition of ControlValueAccessor it mentions that registerOnChange is responsible for view -> model updates (docs where they say it, and src). With a simple directive that we may put on the same input element as [(NgModel)], e.g. <input [(NgModel)]=stuff myInspectorDirective>, I tried playing around with this.
constructor(private ngControl: NgControl) { }
ngOnInit(): void {
// this.ngControl.valueAccessor['onChange'] = () => {};
// uncommenting the above line prevents updates from view to model
}
Uncommenting/commenting the indicated line allows us to allow/block updates from the input element to the model. But I'm puzzled by this because in the source code of DefaultValueAccessor, the one used in this example, onChange is not really doing anything: (_:any) => {}.
So, I would expect that under the hood, e.g. in ng_model.ts or in one of the related classes, like NgControl or FormControl, something happens with the onChange function from the ValueAccessor; setting it or wrapping it in another function, maybe a proxy, or whatever. I did not find anything. Then I went on looking for some code where listeners (for the input event, more explicitly) are explicitly bound to the input element, but no luck either.
I noticed that the OnChanges function calls _setValue, but I'm not sure if I'm going in the right direction when diving into the internals of change detection, as I would expect the listening to changes in the DOM to be related to ControlValueAccessors and/or FormControl/AbstractControl
Anyone feels like elaborating on how this works? :-)

The ControlValueAccessor.registerOnChange is provided by the NgForm.
1) NgModel is registered in NgForm (see https://github.com/angular/angular/blob/master/packages/forms/src/directives/ng_model.ts)
in NgModel.ngOnChanges: this._setUpControl calls this.formDirective.addControl
2) NgForm calls shared setUpControl function (see https://github.com/angular/angular/blob/master/packages/forms/src/directives/ng_form.ts)
import { setUpControl } from './shared';
NgForm.addControl calls setUpControl
3) setUpControl registers change event handler (see https://github.com/angular/angular/blob/master/packages/forms/src/directives/shared.ts)
setUpControl calls setUpViewChangePipeline
function setUpViewChangePipeline(control: FormControl, dir: NgControl): void {
dir.valueAccessor !.registerOnChange((newValue: any) => {
control._pendingValue = newValue;
control._pendingChange = true;
control._pendingDirty = true;
if (control.updateOn === 'change') updateControl(control, dir);
});
}

Related

Angular - RxJS : afterViewInit and Async pipe

I tried to do the following in my component which uses changeDetection: ChangeDetectionStrategy.OnPush,
#ViewChild('searchInput') input: ElementRef;
ngAfterViewInit() {
this.searchText$ = fromEvent<any>(this.input.nativeElement, 'keyup')
.pipe(
map(event => event.target.value),
startWith(''),
debounceTime(300),
distinctUntilChanged()
);
}
And in the template
<div *ngIf="searchText$ | async as searchText;">
results for "<b>{{searchText}}</b>"
</div>
It doesn't work, however if I remove the OnPush, it does. I am not too sure why since the async pipe is supposed to trigger the change detection.
Edit:
Following the answers, I have tried to replace what I have by the following:
this.searchText$ = interval(1000);
Without any #Input, the async pipe is marking my component for check and it works just fine. So I don't get why I haven't got the same behavior with the fromEvent
By default Whenever Angular kicks change detection, it goes through all components one by one and checks if something changes and updates its DOM if it's so. what happens when you change default change detection to ChangeDetection.OnPush?
Angular changes its behavior and there are only two ways to update component DOM.
#Input property reference changed
Manually called markForCheck()
If you do one of those, it will update DOM accordingly. in your case you don't use the first option, so you have to use the second one and call markForCheck(), anywhere. but there is one occasion, whenever you use async pipe, it will call this method for you.
The async pipe subscribes to an Observable or Promise and returns the
latest value it has emitted. When a new value is emitted, the async
pipe marks the component to be checked for changes. When the component
gets destroyed, the async pipe unsubscribes automatically to avoid
potential memory leaks.
so there is nothing magic here, it calls markForCheck() under the hood. but if it's so why doesn't your solution work? In order to answer this question let's dive in into the AsyncPipe itself. if we inspect the source code AsyncPipes transform function looks like this
transform(obj: Observable<any>|Promise<any>|null|undefined): any {
if (!this._obj) {
if (obj) {
this._subscribe(obj);
}
this._latestReturnedValue = this._latestValue;
return this._latestValue;
}
....// some extra code here not interesting
}
so if the value passed is not undefined, it will subscribe to that observable and act accordingly (call markForCheck(), whenever value emits)
Now it's the most crucial part
the first time Angular calls the transform method, it is undefined, because you initialize searchText$ inside ngAfterViewInit() callback (the View is already rendered, so it calls async pipe also). So when you initialize searchText$ field, the change detection already finished for this component, so it doesn't know that searchText$ has been defined, and subsequently it doesn't call AsyncPipe anymore, so the problem is that it never get's to AsyncPipe to subscribe on those changes, what you have to do is call markForCheck() only once after the initialization, Angular ran changeDetection again on that component, update the DOM and call AsyncPipe, which will subscribe to that observable
ngAfterViewInit() {
this.searchText$ =
fromEvent<any>(this.input.nativeElement, "keyup").pipe(
map((event) => event.target.value),
startWith(""),
debounceTime(300),
distinctUntilChanged()
);
this.cf.markForCheck();
}
The changeDetection: ChangeDetectionStrategy.OnPush allow to the component to not triggered the changeDetection all the time but just when an #Input() reference is updated. So if you do all your stuff in the same component, no #Input() reference are updated so the view is not updated.
I propose you to Create your dumb component with your template code above, but give it the searchText via an #Input(), and call your dumb component in your smart component
Smart component
<my-dumb-component [searchText]="searchText$ | async"></my-dumb-component>
Dumb component
#Input() searchText: SearchText
template
<div *ngIf="searchText">
results for "<b>{{searchText}}</b>"
</div>
This is because Angular is updates DOM interpolations before ngAfterViewInit and ngAfterViewChecked. I know this sounds confusing a bit. It's because of the first change detection cycle Angular does. Referring to Max Koretskyi's article about change detection algorithm of Angular, in a change detection cycle these happens sequentially:
sets ViewState.firstCheck to true if a view is checked for the first time and to false if it was already checked before
checks and updates input properties on a child component/directive
instance
updates child view change detection state (part of change detection
strategy implementation)
runs change detection for the embedded views (repeats the steps in
the list)
calls OnChanges lifecycle hook on a child component if bindings
changed
calls OnInit and ngDoCheck on a child component (OnInit is called
only during first check)
updates ContentChildren query list on a child view component
instance
calls AfterContentInit and AfterContentChecked lifecycle hooks on
child component instance (AfterContentInit is called only during
first check)
updates DOM interpolations for the current view if properties on
current view component instance changed
runs change detection for a child view (repeats the steps in this
list)
updates ViewChildren query list on the current view component
instance
calls AfterViewInit and AfterViewChecked lifecycle hooks on child
component instance (AfterViewInit is called only during first
check)
disables checks for the current view (part of change detection
strategy implementation)
As you see, Angular updates DOM interpolations (at step 9) after AfterContentInit and AfterContentChecked hooks are called, so if you call rxjs subscriptions in AfterContentInit or AfterContentChecked lifecycle hooks (or earlier, like OnInit etc.) your DOM will be updated because Angular updates DOM at step 10, and when you change something in ngAfterViewInit() and you are using OnPush, Angular won't update DOM because you are at step 12 on ngAfterViewInit() and Angular has already updated DOM before you change something!
There are workaround solutions to avoid this to subscribe it in ngAfterViewInit. First, you can call markForCheck() function, so you basically say by using it on the first cycle that "hey Angular, you updated DOM on step 9, but I have something to change at step 12, so please be careful, have a look at ngAfterViewInit I have still something to change". Or as a second solution, you can trigger a change detection manually again (by triggering and event handler or using detecthanges() function of ChangeDetectorRef) so that Angular repeats all these steps again, and when it reaches at step 9 again, Angular updates your DOM.
I have created a Stackblitz example that you can try these out. You can uncomment the lines of subscriptions placed in lifecycle hooks 1 by 1, so that you can see after which lifecycle hook Angular updates DOM. Or you can try triggering an event or triggering change detection cycle manually and see that Angular updates DOM on the next cycle.

Angular4 - No value accessor for form control

I have a custom element :
<div formControlName="surveyType">
<div *ngFor="let type of surveyTypes"
(click)="onSelectType(type)"
[class.selected]="type === selectedType">
<md-icon>{{ type.icon }}</md-icon>
<span>{{ type.description }}</span>
</div>
</div>
When I try to add the formControlName, I get an error message:
ERROR Error: No value accessor for form control with name:
'surveyType'
I tried to add ngDefaultControl without success.
It seems it's because there is no input/select... and I dont know what to do.
I would like to bind my click to this formControl in order that when someone clicks on the entire card that would push my 'type' into the formControl. Is it possible?
You can use formControlName only on directives which implement ControlValueAccessor.
Implement the interface
So, in order to do what you want, you have to create a component which implements ControlValueAccessor, which means implementing the following three functions:
writeValue (tells Angular how to write value from model into view)
registerOnChange (registers a handler function that is called when the view changes)
registerOnTouched (registers a handler to be called when the component receives a touch event, useful for knowing if the component has been focused).
Register a provider
Then, you have to tell Angular that this directive is a ControlValueAccessor (interface is not gonna cut it since it is stripped from the code when TypeScript is compiled to JavaScript). You do this by registering a provider.
The provider should provide NG_VALUE_ACCESSOR and use an existing value. You'll also need a forwardRef here. Note that NG_VALUE_ACCESSOR should be a multi provider.
For example, if your custom directive is named MyControlComponent, you should add something along the following lines inside the object passed to #Component decorator:
providers: [
{
provide: NG_VALUE_ACCESSOR,
multi: true,
useExisting: forwardRef(() => MyControlComponent),
}
]
Usage
Your component is ready to be used. With template-driven forms, ngModel binding will now work properly.
With reactive forms, you can now properly use formControlName and the form control will behave as expected.
Resources
Custom Form Controls in Angular by Thoughtram
Angular Custom Form Controls with Reactive Forms and NgModel by Cory Rylan
You should use formControlName="surveyType" on an input and not on a div
The error means, that Angular doesn't know what to do when you put a formControl on a div.
To fix this, you have two options.
You put the formControlName on an element, that is supported by Angular out of the box. Those are: input, textarea and select.
You implement the ControlValueAccessor interface. By doing so, you're telling Angular "how to access the value of your control" (hence the name). Or in simple terms: What to do, when you put a formControlName on an element, that doesn't naturally have a value associated with it.
Now, implementing the ControlValueAccessor interface can be a bit daunting at first. Especially because there isn't much good documentation of this out there and you need to add a lot of boilerplate to your code. So let me try to break this down in some simple-to-follow steps.
Move your form control into its own component
In order to implement the ControlValueAccessor, you need to create a new component (or directive). Move the code related to your form control there. Like this it will also be easily reusable. Having a control already inside a component might be the reason in the first place, why you need to implement the ControlValueAccessor interface, because otherwise you will not be able to use your custom component together with Angular forms.
Add the boilerplate to your code
Implementing the ControlValueAccessor interface is quite verbose, here's the boilerplate that comes with it:
import {Component, OnInit, forwardRef} from '#angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '#angular/forms';
#Component({
selector: 'app-custom-input',
templateUrl: './custom-input.component.html',
styleUrls: ['./custom-input.component.scss'],
// a) copy paste this providers property (adjust the component name in the forward ref)
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}
]
})
// b) Add "implements ControlValueAccessor"
export class CustomInputComponent implements ControlValueAccessor {
// c) copy paste this code
onChange: any = () => {}
onTouch: any = () => {}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouch = fn;
}
// d) copy paste this code
writeValue(input: string) {
// TODO
}
So what are the individual parts doing?
a) Lets Angular know during runtime that you implemented the ControlValueAccessor interface
b) Makes sure you're implementing the ControlValueAccessor interface
c) This is probably the most confusing part. Basically what you're doing is, you give Angular the means to override your class properties/methods onChange and onTouch with it's own implementation during runtime, such that you can then call those functions. So this point is important to understand: You don't need to implement onChange and onTouch yourself (other than the initial empty implementation). The only thing your doing with (c) is to let Angular attach it's own functions to your class. Why? So you can then call the onChange and onTouch methods provided by Angular at the appropriate time. We'll see how this works down below.
d) We'll also see how the writeValue method works in the next section, when we implement it. I've put it here, so all required properties on ControlValueAccessor are implemented and your code still compiles.
Implement writeValue
What writeValue does, is to do something inside your custom component, when the form control is changed on the outside. So for example, if you have named your custom form control component app-custom-input and you'd be using it in the parent component like this:
<form [formGroup]="form">
<app-custom-input formControlName="myFormControl"></app-custom-input>
</form>
then writeValue gets triggered whenever the parent component somehow changes the value of myFormControl. This could be for example during the initialization of the form (this.form = this.formBuilder.group({myFormControl: ""});) or on a form reset this.form.reset();.
What you'll typically want to do if the value of the form control changes on the outside, is to write it to a local variable which represents the form control value. For example, if your CustomInputComponent revolves around a text based form control, it could look like this:
writeValue(input: string) {
this.input = input;
}
and in the html of CustomInputComponent:
<input type="text"
[ngModel]="input">
You could also write it directly to the input element as described in the Angular docs.
Now you have handled what happens inside of your component when something changes outside. Now let's look at the other direction. How do you inform the outside world when something changes inside of your component?
Calling onChange
The next step is to inform the parent component about changes inside of your CustomInputComponent. This is where the onChange and onTouch functions from (c) from above come into play. By calling those functions you can inform the outside about changes inside your component. In order to propagate changes of the value to the outside, you need to call onChange with the new value as the argument. For example, if the user types something in the input field in your custom component, you call onChange with the updated value:
<input type="text"
[ngModel]="input"
(ngModelChange)="onChange($event)">
If you check the implementation (c) from above again, you'll see what's happening: Angular bound it's own implementation to the onChange class property. That implementation expects one argument, which is the updated control value. What you're doing now is you're calling that method and thus letting Angular know about the change. Angular will now go ahead and change the form value on the outside. This is the key part in all this. You told Angular when it should update the form control and with what value by calling onChange. You've given it the means to "access the control value".
By the way: The name onChange is chosen by me. You could choose anything here, for example propagateChange or similar. However you name it though, it will be the same function that takes one argument, that is provided by Angular and that is bound to your class by the registerOnChange method during runtime.
Calling onTouch
Since form controls can be "touched", you should also give Angular the means to understand when your custom form control is touched. You can do it, you guessed it, by calling the onTouch function. So for our example here, if you want to stay compliant with how Angular is doing it for the out-of-the-box form controls, you should call onTouch when the input field is blurred:
<input type="text"
[(ngModel)]="input"
(ngModelChange)="onChange($event)"
(blur)="onTouch()">
Again, onTouch is a name chosen by me, but what it's actual function is provided by Angular and it takes zero arguments. Which makes sense, since you're just letting Angular know, that the form control has been touched.
Putting it all together
So how does that look when it comes all together? It should look like this:
// custom-input.component.ts
import {Component, OnInit, forwardRef} from '#angular/core';
import {ControlValueAccessor, FormControl, NG_VALUE_ACCESSOR} from '#angular/forms';
#Component({
selector: 'app-custom-input',
templateUrl: './custom-input.component.html',
styleUrls: ['./custom-input.component.scss'],
// Step 1: copy paste this providers property
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}
]
})
// Step 2: Add "implements ControlValueAccessor"
export class CustomInputComponent implements ControlValueAccessor {
// Step 3: Copy paste this stuff here
onChange: any = () => {}
onTouch: any = () => {}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouch = fn;
}
// Step 4: Define what should happen in this component, if something changes outside
input: string;
writeValue(input: string) {
this.input = input;
}
// Step 5: Handle what should happen on the outside, if something changes on the inside
// in this simple case, we've handled all of that in the .html
// a) we've bound to the local variable with ngModel
// b) we emit to the ouside by calling onChange on ngModelChange
}
// custom-input.component.html
<input type="text"
[(ngModel)]="input"
(ngModelChange)="onChange($event)"
(blur)="onTouch()">
// parent.component.html
<app-custom-input [formControl]="inputTwo"></app-custom-input>
// OR
<form [formGroup]="form" >
<app-custom-input formControlName="myFormControl"></app-custom-input>
</form>
More Examples
Example with Input: https://stackblitz.com/edit/angular-control-value-accessor-simple-example-tsmean
Example with Lazy Loaded Input: https://stackblitz.com/edit/angular-control-value-accessor-lazy-input-example-tsmean
Example with Button: https://stackblitz.com/edit/angular-control-value-accessor-button-example-tsmean
Nested Forms
Note that Control Value Accessors are NOT the right tool for nested form groups. For nested form groups you can simply use an #Input() subform instead. Control Value Accessors are meant to wrap controls, not groups! See this example how to use an input for a nested form: https://stackblitz.com/edit/angular-nested-forms-input-2
Sources
https://angular.io/api/forms/ControlValueAccessor
https://www.tsmean.com/articles/angular/angular-control-value-accessor-example/
For me it was due to "multiple" attribute on select input control as Angular has different ValueAccessor for this type of control.
const countryControl = new FormControl();
And inside template use like this
<select multiple name="countries" [formControl]="countryControl">
<option *ngFor="let country of countries" [ngValue]="country">
{{ country.name }}
</option>
</select>
More details ref Official Docs

Prevent ngOnChanges from firing after emitting event (Angular 2+)

In Angular 2+, custom two-way databinding can be accomplished by using #Input and #Output parameters. So if I want a child component to communicate with a third party plugin, I could do it as follows:
export class TestComponent implements OnInit, OnChanges {
#Input() value: number;
#Output() valueChange = new EventEmitter<number>();
ngOnInit() {
// Create an event handler which updates the parent component with the new value
// from the third party plugin.
thirdPartyPlugin.onSomeEvent(newValue => {
this.valueChange.emit(newValue);
});
}
ngOnChanges() {
// Update the third party plugin with the new value from the parent component
thirdPartyPlugin.setValue(this.value);
}
}
And use it like this:
<test-component [(value)]="value"></test-component>
After the third party plugin fires an event to notify us of a change, the child component updates the parent component by calling this.valueChange.emit(newValue). The issue is that ngOnChanges then fires in the child component because the parent component's value has changed, which causes thirdPartyPlugin.setValue(this.value) to be called. But the plugin is already in the correct state, so this is a potentially unnecessary/expensive re-render.
So what I often do is create a flag property in my child component:
export class TestComponent implements OnInit, OnChanges {
ignoreModelChange = false;
ngOnInit() {
// Create an event handler which updates the parent component with the new value
// from the third party plugin.
thirdPartyPlugin.onSomeEvent(newValue => {
// Set ignoreModelChange to true if ngChanges will fire, so that we avoid an
// unnecessary (and potentially expensive) re-render.
if (this.value === newValue) {
return;
}
ignoreModelChange = true;
this.valueChange.emit(newValue);
});
}
ngOnChanges() {
if (ignoreModelChange) {
ignoreModelChange = false;
return;
}
thirdPartyPlugin.setValue(this.value);
}
}
But this feels like a hack.
In Angular 1, directives which took in a parameter using the = binding had the same exact issue. So instead, I would accomplish custom two-way databinding by requiring ngModelController, which did not cause a re-render after a model update:
// Update the parent model with the new value from the third party plugin. After the model
// is updated, $render will not fire, so we don't have to worry about a re-render.
thirdPartyPlugin.onSomeEvent(function (newValue) {
scope.$apply(function () {
ngModelCtrl.$setViewValue(newValue);
});
});
// Update the third party plugin with the new value from the parent model. This will only
// fire if the parent scope changed the model (not when we call $setViewValue).
ngModelCtrl.$render = function () {
thirdPartyPlugin.setValue(ngModelCtrl.$viewValue);
};
This worked, but ngModelController really seems to be designed for form elements (it has built in validation, etc.). So it felt a bit odd to use it in custom directives which are not form elements.
Question: Is there a best practice in Angular 2+ for implementing custom two-way databinding in a child component, which does not trigger ngOnChanges in the child component after updating the parent component using EventEmitter? Or should I integrate with ngModel just as I did in Angular 1, even if my child component is not a form element?
Thanks in advance!
Update: I checked out Everything you need to know about change detection in Angular suggested by #Maximus in the comments. It looks like the detach method on ChangeDetectorRef will prevent any bindings in the template from being updated, which could help with performance if that's your situation. But it does not prevent ngOnChanges from being called:
thirdPartyPlugin.onSomeEvent(newValue => {
// ngOnChanges will still fire after calling emit
this.changeDetectorRef.detach();
this.valueChange.emit(newValue);
});
So far I haven't found a way to accomplish this using Angular's change detection (but I learned a lot in the process!).
I ended up trying this with ngModel and ControlValueAccessor. This seems to accomplish what I need since it behaves as ngModelController in Angular 1:
export class TestComponentUsingNgModel implements ControlValueAccessor, OnInit {
value: number;
// Angular will pass us this function to invoke when we change the model
onChange = (fn: any) => { };
ngOnInit() {
thirdPartyPlugin.onSomeEvent(newValue => {
this.value = newValue;
// Tell Angular to update the parent component with the new value from the third
// party plugin
this.onChange(newValue);
});
}
// Update the third party plugin with the new value from the parent component. This
// will only fire if the parent component changed the model (not when we call
// this.onChange).
writeValue(newValue: number) {
this.value = newValue;
thirdPartyPlugin.setValue(this.value);
}
registerOnChange(fn: any) {
this.onChange = fn;
}
}
And use it like this:
<test-component-using-ng-model [(ngModel)]="value"></test-component-using-ng-model>
But again, if the custom component is not a form element, using ngModel seems a bit odd.
Also ran into this problem (or at least something very similar).
I ended up using hacky approach you discussed above but with a minor modification, I used setTimeout in order to reset state just in case.
(For me personally ngOnChanges was mainly problematic if using two-way binding, so the setTimeout prevents a hanging disableOnChanges if NOT using two-way binding).
changePage(newPage: number) {
this.page = newPage;
updateOtherUiVars();
this.disableOnChanges = true;
this.pageChange.emit(this.page);
setTimeout(() => this.disableOnChanges = false, 0);
}
ngOnChanges(changes: any) {
if (this.disableOnChanges) {
this.disableOnChanges = false;
return;
}
updateOtherUiVars();
}
This is exactly the intention of Angular and its something you should try to work with rather than against. Change detection works by components detecting changes in its template bindings and propagating them down the component tree. If you can design your application in such a way that you are relying on the immutability of components inputs', you can control this manually by setting #Component({changeDetection:ChangeDetectionStrategy.OnPush}) which will test references to determine whether to continue change detection on children components.
So, that said, my experience is that wrappers of 3rd party plugins may not efficiently handle and take advantage of this type of strategy appropriately. You should attempt to use knowledge of the above, together with good design choices like the separation of concerns of presentation vs container components to leverage the detection strategy to achieve good performance.
You can also pass changes: SimpleChanges to ngOnInit(changes: SimpleChanges) and inspect the object to learn more about your data flow.

Why does component view update when change detection is set to onPush? [duplicate]

I thought I was pretty clear on how Angular Change detection works after this discussion: Why is change detection not happening here when [value] changed?
But take a look at this plunk: https://plnkr.co/edit/jb2k7U3TfV7qX2x1fV4X?p=preview
#Component({
selector: 'simple',
template: `
<div (click)="onClick()">
{{myData[0].name}}
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class Simple {
public #Input() myData;
constructor() {
}
public onClick() {
}
}
Click on a, it's changed to c
I understand that the click event triggers change detection on the App level, but [myData]="testData" is still referring to the same object, and I am using On Push on Simple, why does a get changed?
That's by design.
If you have component with OnPush change detection then its detectChangesInternal function won't be triggered unless one of four things happens:
1) one of its #Inputs changes
~2.4.x
~4.x.x
Note: #Inputs should be presented in template. See issue https://github.com/angular/angular/issues/20611 and comment
2) a bound event is triggered from the component (that is your case)
Caveats: There is some difference here between 2.x.x and 4
Angular ChangeDetectionStrategy.OnPush with child component emitting an event
~2.4.x
~4.x.x
3) you manually mark the component to be checked (ChangeDetectorRef.markForCheck())
4) async pipe calls ChangeDetectorRef.markForCheck() internally
private _updateLatestValue(async: any, value: Object): void {
if (async === this._obj) {
this._latestValue = value;
this._ref.markForCheck();
}
}
https://github.com/angular/angular/blob/2.4.8/modules/%40angular/common/src/pipes/async_pipe.ts#L137
In other words if you set OnPush for component then after the first checking component's status will be changed from CheckOnce to Checked and after that it's waiting as long as we do not change status. It will happen in one of three things above.
See also:
https://github.com/angular/angular/issues/11678#issuecomment-247894782
There are also good explanations of how angular2 change detection work:
https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html
https://hackernoon.com/everything-you-need-to-know-about-change-detection-in-angular-8006c51d206f
Here is Live Example(Thanks to Paskal) that explains onPush change detection. (Comp16 looks like your component. You can click at this box).

Angular2, doe's EventEmitter require zone.run?

I'm using ionic2, I implemented a class:
import {EventEmitter, Injectable} from 'angular2/core';
#Injectable()
export class LocalPushClear extends EventEmitter<number> {
constructor() {
super();
}
}
The class is used by on of my components to connect cordova plugin event to another component which subscribe to LocalPushClear, I listen to clear events, ones it fires, I emit using LocalPushClear and some other component subscribes:
this._LocalPushClear.subscribe(data => {
// Some action is taken here
});
The thing is that, I was expecting automatic change detection to be executed upon subscription callback execution(when its done), but it seems like there is no change detection execution at all, I have to do something like click a button or wrap my Some action with zone.run, I'm not sure if its a valid behavior or maybe I'm doing something wrong.
Edit:
I traces the code and it leads to Subject, so its basically custom event emitter that angular NgZone don't know about(at least I think), but I'm sure, if anyone could confirm, maybe future explain I will be very thankful.
You definitely should not extend EventEmitter. EventEmitter is only supposed to be used for #Output()s. Just use a Subject instead.
Angular doesn't get notified about values emitted by EventEmitter (when used this way) or Subject. Normally the code that causes the Observable (Subject) to emit new values is executed by code that causes change detection when completed for example when called from an event handler or setTimeout.
In your case the cause seems to be that the code that emits new values using LocalPushClear runs outside Angulars zone.
You can use one of the methods explained in https://stackoverflow.com/a/34829089/217408 to trigger change detection after the Observable emits an event.

Categories

Resources