Angular 4.3.1 update component from inside observable callback [duplicate] - javascript

This question already has answers here:
How to access the correct `this` inside a callback
(13 answers)
Closed 5 years ago.
THE SETUP
I'm teaching myself Angular with a simple app of my own: a game which will use routing, services, etc. On the landing page ( route for '/'), I want the header to hide. The header and router-outlet are together in the app-component like this (including some debug output in the brand):
<nav class="navbar navbar-default navbar-fixed-top" [hidden]="!showHeader">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="" [routerLink]="['/']">FARKLE! {{showHeader | json}}</a>
</div>
</div>
</nav>
<div class="container">
<router-outlet></router-outlet>
</div>
The component class declares the boolean showHeader and attempts to update it when the route changes like this:
import { Component, OnInit } from '#angular/core';
import { Router, NavigationEnd } from '#angular/router';
#Component({
selector: 'app-root',
templateUrl: './app.component.html'
})
export class AppComponent implements OnInit {
showHeader: boolean;
constructor(
private router: Router
) {}
ngOnInit () {
this.showHeader = true;
this.router.events.subscribe(this.setHeader());
}
private setHeader () {
var showHeader = this.showHeader;
return function (event) {
if (event instanceof NavigationEnd) {
showHeader = event.url === '/';
console.log(event.url, showHeader, this.showHeader);
};
};
}
}
The value of this.showHeader properly sets in ngOnInit and the header will show or not show correctly depending on how it is initialized. The console shows the event occurring during navigation and the value being determined correctly. The issue is in that in the context of the callback, this is no longer the component. So, I attempt to pass this.showHeader in by reference. BUT, showHeader is not reflected in the template (most likely because it is not actually getting into the scope of the event callback.
THE QUESTION
So, how do you affect the component scope from with the callback of the observable?

What I understood is, you're kind of doing closure, where the setHeader's returned function will be registered as subscription.
The setHeader's returned function should use Arrow function in order to persist correct this
private setHeader () {
var showHeader = this.showHeader;
//return function (event) {
//should get changed to below line
return (event) => { //Use Arrow function here.
if (event instanceof NavigationEnd) {
showHeader = event.url === '/';
console.log(event.url, showHeader, this.showHeader);
};
};
}

You need to bind the this keyword to the setHeader function.
You can do that in 2 ways:
use arrow => function
this.router.events.subscribe(() => this.setHeader());
use bind
this.router.events.subscribe(this.setHeader.bind(this));

Related

how can pass different parameters using [routerLink] or router.navigate to a component?

I configured app-routing.module.ts as following:
const routes: Routes = [
{
path: "branches/:branch",
component: BranchesComponent
},
// ...
];
and also in app.component.html have:
<li>
<a [routerLink]="['/branches', 'engineering']"> engineering </a>
</li>
<li>
<a [routerLink]="['/branches', 'baseSciense']"> baseSciense</a>
</li>
<li>
<a [routerLink]="['/branches', 'humanities']"> humanities</a>
</li>
</ul>
<router-outlet></router-outlet>
and in the barnches.component.ts I coded as bellow:
branch: string ='';
constructor(private route: ActivatedRoute) { }
ngOnInit(): void {
this.route.params.subscribe(({branch: branch1}) => this.branch = branch1);
// I also tried this code:
// this.route.params.subscribe(branch => this.branch = branch['branch']);
// unfortunately, bellow codes have error on p.branch and params.branch! why?
// this.route.params.subscribe(p => this.branch = p.branch)
// this.branch = this.route.snapshot.params.branch;
console.log(`branch is : ${this.branch}`);
}
till here everything comes correct, and URL is changed once the respective link is clicked, such as :
http://localhost:4200/branches/engineering
http://localhost:4200/branches/baseSciense
http://localhost:4200/branches/humanities
but the property branch in Branches component is not changed and has same value (engineering) for different parameters in log of the console. it is illogical for me!
how can solve this problem as pass different parameters to and capture them into branches component? Best regards
You need to move your console.log within the subscription. Most of the code related to this feature will need to take place within the subscription. The Component doesn't re-render on url change because it is loading the same component and angular doesn't re-render components if it is the same component.
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
#Component({
template: 'Branch: {{branch}}',
})
export class BranchesComponent implements OnInit {
branch = '';
constructor(private route: ActivatedRoute) {}
ngOnInit() {
// Set the value every time the URL param changes.
this.route.params.subscribe(({ branch }) => {
this.branch = branch;
console.log('branch:', this.branch);
});
}
}
In your component you can subscribe to router events and listen for them like this.
this.router.events.pipe(filter(r => r instanceof NavigationEnd)).subscribe(r => {
console.log((r as NavigationEnd).url);
});

Angular 6 nested ViewChild inside ng-template is null

We are using a modal (ng-bootstrap's one) in our application. That modal looks like:
<ng-template #modal let-modal>
<app-audio #audio></app-audio>
</ng-template>
And it's logic:
#ViewChild('modal')
modal: ElementRef;
#ViewChild('audio')
audio: AudioComponent;
The modal is opened with:
this.modalService.open(this.modal, { size: 'lg' });
Everything fine till here. The modal opens and the audio component is shown. But now, we want to access the logic that is inside the component, and when doing something like:
this.audio.somePublicComponentFunction()
It happens that this.audio is null. I have already tried to get the child with angular's change detector, but cannot find a way to properly link this.audio with the actual component. Any ideas? Thanks a lot.
You can see the issue here: stackblitz.com/edit/angular-ofmpju
You can call the method audio.someFunction() from the template itself.
<ng-template #modal let-modal>
<div style="background-color: red;">
<h1>Modal header</h1>
<app-audio #audio></app-audio>
<!-- on click, call audio comp method someFunction() using its reference -->
<button (click)="audio.someFunction()">Operate with audio from inside modal</button>
</div>
</ng-template>
No need of #ViewChild property here. This should do the trick for you.
Forked demo
You can read the child component without the refrence variable like this
#ViewChild(AudioComponent)
audio: AudioComponent;
This will give you the instance of the child component - where you can access the method
this.audio.someComponentFunction()
Your html
<ng-template #modal let-modal>
<app-audio></app-audio>
</ng-template>
This will solve your issue i think - Happy coding
Update:
Hope i found a workaround for this issue - if in case you want to trigger only one function you can use this method
I have just added a property with getter and setter and triggered the function when we set the value
#Input()
get triggerFunction(): boolean {
return this.runFuntion;
}
set triggerFunction(value: boolean) {
this.runFuntion = value;
this.someFunction();
}
So this causes to trigger the function every time when the model show up - property mentioned above belongs to the child component which is nested inside the <ng-template> so finally the model template will read as mentioned below:
<ng-template #modal let-modal>
<app-audio [triggerFunction]="true"></app-audio>
</ng-template>
Hope this will act a workaround for now - Thanks
For me all this solutions did not work and I still wanted to access my own component inside a third party ng-template. Here is my 'solution'. I don't think this is best practice but a desperate solution to get what I want ;-) It only works for your own components of course.
// mycomponent.ts => component that needs to be accessed
import { Component, Output, EventEmitter, AfterViewInit } from '#angular/core';
#Component({
selector: 'my-component',
templateUrl: './mycomponent.html'
})
export class MyComponent implements AfterViewInit {
#Output() initialized: EventEmitter<MyComponent> = new EventEmitter<MyComponent>();
ngAfterViewInit(): void {
this.initialized.emit(this);
}
reload(): void {
// Do something
}
}
// somecomponent.html => component with <ng-template> holding MyComponent
<ng-template>
<div class="btn-group ml-2">
<my-component (initialized)="onMyComponentInitialized($event)"></my-component>
</div>
</ng-template>
// somecomponent.ts => component with <ng-template> holding MyComponent
import { Component, OnDestroy } from '#angular/core';
import { MyComponent } from '../../my-component';
#Component({
selector: 'some-component',
templateUrl: './some-component.html'
})
export class SomeComponent implements OnDestroy {
private _myComponent: MyComponent = null;
onMyComponentInitialized(component: MyComponent) {
this._myComponent = component;
}
someOtherMethod() {
if (this._myComponent) {
// Call some method on the component
this._myComponent.reload();
}
}
ngOnDestroy() {
this._myComponent = null;
}
}

Angular 4 ngOnInit not working when loading the component first time

I Have a component in Angular 4 that and a template to change the route
This component is called but does not load anything no server call.
If i put the ngOnInit() method content into constructor it works fine.
It seems ngOnInit is not called. Anybody please can help i am working on this since last 2 days.
here is my routing configuration.
const testRouting: ModuleWithProviders = RouterModule.forChild([
{ path:'createtest/:id', component:TestComponent, resolve: { test:TestResolver }},
{ path:'createtest', component:TestComponent, resolve: { test:TestResolver }},
{ path:'testlist', component:TestListComponent }
]);
import {Component,OnInit} from '#angular/core'
import {TestService,Test} from '../shared'
import 'rxjs/add/operator/map';
#Component({
selector:'test-list',
templateUrl:'./testlist.component.html'
})
export class TestListComponent implements OnInit{
testList:Array<Test>;
constructor(private testService:TestService){}
ngOnInit = ()=>{
this.testService.getTest()
.subscribe(
data=>this.testList = <Array<Test>>data,
error=>alert(error)
);
console.log("ngOnInit");
}
}
And here is my template to configure routing
<nav class="navbar navbar-light">
<div class="container">
<a class="navbar-brand" routerLink="/">SLearn</a>
<a class="xyz" routerLink="/testlist">Test List</a>
</div>
</nav>
You have to overload the ngOnInit function. This doesn't work with arrow functions.
You have to do:
ngOnInit() {
this.testService.getTest()
.subscribe(
data=>this.testList = <Array<Test>>data,
error=>alert(error)
);
console.log("ngOnInit");
}
Hope i could help.
Update (Additional information thanks to maximus):
While a normal function declaration creates the ngOnInit property on the prototype, an arrow function creates it on the instance.
Angular itself looks only for the hooks on the prototype. which is why your original approach doesn't work as expected.

Angular 2 runOutsideAngular still change the UI

From my understanding of runOutsideAngular(), if I need to run something that won't trigger the Angular change detection, I need to use this function. My code is not working, however; when I click the button, the UI is changing and the number is 2.
#Component({selector: 'my-cmp',
template: `<h1>{{num}}</h1>
<button (click)="onClick()">Change number</button>`})
class MyComponent implements OnChanges {
num = 1;
constructor(private _ngZone: NgZone ) {
}
onClick() {
this._ngZone.runOutsideAngular(() => {
this.num = 2;
}}));
}
}
If anything is causing change detection, and a bound event like (click)="onClick()" does cause change detection, then Angular will detect the change.
runOutsideAngular doesn't mean Angular won't see the change, it only means that the code run this way doesn't cause change detection, but because the click event already does, it's meaningless in your example.
[In short] you need to change one line in your current code
onClick() {
this._ngZone.runOutsideAngular(() => {
setTimeout(()=>this.num = 2,0); // instead of this.num = 2;
}}));
}
now if you click the on the <button>, this.num will become 2, but you won't see any change in the UI (a temporary inconsistency between view and model)
[Explanation] without runOutsideAngular(), async functions like addEventListener() or setTimeout() behaves differently (monkey patched). their callbacks will try to update UI with Angular after running user's code.
For example, you can treat (click)="onClick()" as:
addEventListener("click",function modifiedCallback(){
onClick();
updateUIifModelChanges(); //call to Angular
})
In order to not triggered UI update we need to satisfy the following two conditions:
not modify model in function onClick (so, modify inside setTimeout())
when the model is indeed modified, do not invoke updateUIifModelChanges (call setTimeout() inside runOutsideAngular)
[More] of cause, the explanation I gave is a very very...simplified version of what happens. setTimeout() has the same function signature whether it's running inside runOutsideAngular() or not. The reason that it behaves differently is because it's running in a different Zone
If you want to prevent change detection then you can
1) subscribe on ngZone.onMicrotaskEmpty like this:
import { NgZone, ChangeDetectorRef } from '#angular/core';
import 'rxjs/add/operator/first';
...
export class MyComponent {
constructor(private ngZone: NgZone, private cdRef: ChangeDetectorRef) {}
onClick() {
// to do something
this.cdRef.detach();
this.ngZone.onMicrotaskEmpty.first().subscribe(() => {
// reattach changeDetector after application.tick()
this.cdRef.reattach();
});
}
}
This handler will run after Application.tick
See also Plunker Example
2) use custom directive like this:
#Directive({
selector: '[outSideEventHandler]'
})
class OutSideEventHandlerDirective {
private handler: Function;
#Input() event: string = 'click'; // pass desired event
#Output('outSideEventHandler') emitter = new EventEmitter();
constructor(private ngZone: NgZone, private elRef: ElementRef) {}
ngOnInit() {
this.ngZone.runOutsideAngular(() => {
this.handler = $event => this.emitter.emit($event);
this.elRef.nativeElement.addEventListener(this.event, this.handler);
});
}
ngOnDestory() {
this.elRef.nativeElement.removeEventListener(this.event, this.handler);
}
}
and then in template you can write:
<button (outSideEventHandler)="onClick()">Click outside zone</button>
or
<button event="mousedown" (outSideEventHandler)="onClick()">Click outside zone</button>
Plunker
3) write custom DOM event handler as described in this article.
https://medium.com/#TheLarkInn/creating-custom-dom-events-in-angular2-f326d348dc8b#.bx4uggfdy
Other solutions see here:
Angular 2 how to keep event from triggering digest loop/detection cycle?
Using ngZone.run is a bit better than the setTimeout solutions since it uses angular specific functionality. Run is meant to be used within ngZone.runOutsideAngular functions.
From the docs:
Running functions via run allows you to reenter Angular zone from a
task that was executed outside of the Angular zone (typically started
via {#link #runOutsideAngular}).
This is actually a very practical example of say a button that increments a number by one but only triggers change detection when the number is even.
#Component({selector: 'my-cmp',
template: `<h1>{{num}}</h1>
<button (click)="onClick()">Change number</button>`})
class MyComponent implements OnChanges {
num = 1;
constructor(private _ngZone: NgZone ) {
}
onClick() {
this._ngZone.runOutsideAngular(() => {
if(this.num % 2 === 0){
// modifying the state here wont trigger change.
this.num++;
} else{
this._ngZone.run(() => {
this.num++;
})
}
}}));
}
}
...
constructor(
private ngZone: NgZone
){
ngZone.runOutsideAngular(() => {
setInterval(()=>{
this.num= new Date().Format('yyyy-MM-dd HH:mm:ss');
},1000);
});
}
...
This is how I have tried to check the difference insideAngular and OutsideAngular
constructor(private zone: NgZone) { }
setProgressOutsideAngular() {
this.zone.runOutsideAngular(() => {
setInterval(() => { ++this.progress, console.log(this.progress) }, 500)
})
}
setProgressInsideAngular() {
this.zone.run(() => setInterval(() => { ++this.progress, console.log(this.progress) }, 500))
}

how can I listen to changes in code in angular 2?

I'm using angular 2. I have a component with an input.
I want to be able to write some code when the input value changes.
The binding is working, and if the data is changed (from outside the component) I can see that there is change in the dom.
#Component({
selector: 'test'
})
#View({
template: `
<div>data.somevalue={{data.somevalue}}</div>`
})
export class MyComponent {
_data: Data;
#Input()
set data(value: Data) {
this.data = value;
}
get data() {
return this._data;
}
constructor() {
}
dataChagedListener(param) {
// listen to changes of _data object and do something...
}
}
You could use the lifecycle hook ngOnChanges:
export class MyComponent {
_data: Data;
#Input()
set data(value: Data) {
this.data = value;
}
get data() {
return this._data;
}
constructor() {
}
ngOnChanges([propName: string]: SimpleChange) {
// listen to changes of _data object and do something...
}
}
This hook is triggered when:
if any bindings have changed
See these links for more details:
https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html
https://angular.io/docs/ts/latest/api/core/OnChanges-interface.html
As mentioned in the comments of Thierry Templier's answer, ngOnChanges lifecycle hook can only detect changes to primitives. I found that by using ngDoCheck instead, you are able to check the state of the object manually to determine if the object's members have changed:
A full Plunker can be found here. But here's the important part:
import { Component, Input } from '#angular/core';
#Component({
selector: 'listener',
template: `
<div style="background-color:#f2f2f2">
<h3>Listener</h3>
<p>{{primitive}}</p>
<p>{{objectOne.foo}}</p>
<p>{{objectTwo.foo.bar}}</p>
<ul>
<li *ngFor="let item of log">{{item}}</li>
</ul>
</div>
`
})
export class ListenerComponent {
#Input() protected primitive;
#Input() protected objectOne;
#Input() protected objectTwo;
protected currentPrimitive;
protected currentObjectOne;
protected currentObjectTwo;
protected log = ['Started'];
ngOnInit() {
this.getCurrentObjectState();
}
getCurrentObjectState() {
this.currentPrimitive = this.primitive;
this.currentObjectOne = _.clone(this.objectOne);
this.currentObjectTwoJSON = JSON.stringify(this.objectTwo);
}
ngOnChanges() {
this.log.push('OnChages Fired.')
}
ngDoCheck() {
this.log.push('DoCheck Fired.');
if (!_.isEqual(this.currentPrimitive, this.primitive)){
this.log.push('A change in Primitive\'s state has occurred:');
this.log.push('Primitive\'s new value:' + this.primitive);
}
if(!_.isEqual(this.currentObjectOne, this.objectOne)){
this.log.push('A change in objectOne\'s state has occurred:');
this.log.push('objectOne.foo\'s new value:' + this.objectOne.foo);
}
if(this.currentObjectTwoJSON != JSON.stringify(this.objectTwo)){
this.log.push('A change in objectTwo\'s state has occurred:');
this.log.push('objectTwo.foo.bar\'s new value:' + this.objectTwo.foo.bar);
}
if(!_.isEqual(this.currentPrimitive, this.primitive) || !_.isEqual(this.currentObjectOne, this.objectOne) || this.currentObjectTwoJSON != JSON.stringify(this.objectTwo)) {
this.getCurrentObjectState();
}
}
It should be noted that the Angular documentation provides this caution about using ngDoCheck:
While the ngDoCheck hook can detect when the hero's name has changed,
it has a frightful cost. This hook is called with enormous frequency —
after every change detection cycle no matter where the change
occurred. It's called over twenty times in this example before the
user can do anything.
Most of these initial checks are triggered by Angular's first
rendering of unrelated data elsewhere on the page. Mere mousing into
another input box triggers a call. Relatively few calls reveal actual
changes to pertinent data. Clearly our implementation must be very
lightweight or the user experience will suffer.

Categories

Resources