Angular does not detect changes made from window global function - javascript

During the work I encountered a very strange behaviour.
Here is the link for a similar problem: stackblitz
From index.html file I have raised some click event
function createClause(event) {
Office.context.document.getSelectedDataAsync(
Office.CoercionType.Text,
(asyncResult) => {
window.sendSelectedTextCallback({selectedText: asyncResult.value});
event.completed();
});
}
In the app.component.ts I'm listening to the sendSelectedTextCallback function.
(window as any).sendSelectedTextCallback = (params: any) => {
clauseCommunicationService.addClause({name: params.selectedText});
};
clauseCommunicationService.addClause method calls next function for a subject.
In some component I'm listening for the changes.
this.clauseAddedSubscription = clauseCommunicationService.clauseAdded$.subscribe(
(clause) => {
this.clauses.push(clause);
console.log(this.clauses);
}
);
The issue i'm facing is that console.log(this.clauses) command shows me the list updated, but this is not reflected on the UI.
If I'm replacing
(window as any).sendSelectedTextCallback = (params: any) => {
clauseCommunicationService.addClause({name: params.selectedText});
}
with
setTimeout(() => {
clauseCommunicationService.addClause({name: 'helloooo'});
}, 4000);
i can see that the changes are reflected on the UI.
I tried to use ngZone and ChangeDetector features but without success.

The issue is because the button is outside of angular, so angular doesn't test for change detection on the button so when the button gets clicked it doesn't know something happened. We can add change detection to that button using a #HostListener Stack Blitz example.
export class HelloComponent {
public elements = [1, 2, 3, 4];
#HostListener('window:click')
private onWindowClick() {
this.cdr.detectChanges();
}
constructor(
#Inject(CommunicationService)
private communicationService: CommunicationService,
private cdr: ChangeDetectorRef
) {}
ngOnInit() {
this.communicationService.elemAdded$.subscribe(elem => {
this.elements.push(elem);
console.log(this.elements);
});
}
}

You can force angular to "refresh" and take into consideration events and things that are out of its world by using ngZone : try this
import { NgZone } from '#angular/core';
// In the constructor, inject NgZone
constructor(private zone: NgZone) { }
// Then :
clauseCommunicationService.clauseAdded$.subscribe(
(clause) => {
this.zone.run(() => { // Where the magic happens
this.clauses.push(clause);
console.log(this.clauses);
})
}
);

Related

Passing void events from parent component to child component

Quick overview
I have a "Terminal" Component which should be able to be used multiple times all over my application. This component should also be able to put into a "read only" mode where you pass a single command you'd like the terminal to fire and it will display the output. I am trying to have this component be able to be refreshed by many things; data updating else where, user events, etc. To achieve this, currently, I am using RxJS subjects as an #Input into the terminal component which when updated fires the subscribed functions. This works for the first user click (see bellow) but after that the subject doesn't update again. I suspect this is due to the "object" not updating, there for angular doesn't register the change and my whole idea falls apart.
Can I fix this? or do I need to redesign this "Terminal" component?
Code
terminal.component.ts
export class TerminalComponent implements OnInit, OnDestroy {
constructor() {}
$destroy = new Subject();
terminalOutput = '';
// Command input (if you want the terminal to only fire one command)
#Input() command = '';
$command: BehaviorSubject<string> = new BehaviorSubject('');
// Refresh terminal input
$refresh: Subject<void> = new Subject();
#Input() set refresh(value: Subject<void>) {
this.$refresh = value;
}
// ReadOnly Input
#Input() readOnly = false;
ngOnInit(): void {
this.$refresh
.pipe(
takeUntil(this.$destroy),
tap(() => {
const lastCommand = this.$command.getValue();
if (lastCommand) {
console.log('Refreshing, last command is:', lastCommand);
}
})
)
.subscribe();
//...
}
//...
}
parent.component.html
<h1>Home</h1>
<app-terminal command="ls" [refresh]="$refreshSubject"></app-terminal>
<button (click)="refreshTest()">Refresh</button>
parent.component.ts
export class ParentComponent implements OnInit {
$refreshSubject: Subject<void> = new Subject();
constructor() {}
ngOnInit(): void {}
refreshTest(): void {
console.log('Refreshing');
this.$refreshSubject.next();
}
}
So I found the problem, it was another bug in my code that was causing the RxJS tap to not fire after the first time.
ngOnInit(): void {
this.$refresh
.pipe(
takeUntil(this.$destroy),
tap(() => {
const lastCommand = this.$command.getValue();
if (lastCommand) {
console.log('Refreshing, last command is:', lastCommand);
throw new Error("Example Error");
// ^^^^ This will cause future Subject emits to not fire as this function has failed.
}
})
)
.subscribe();
//...
}
Side note: I think the question title should be changed to better suit the real problem with my code, and there for have better SEO. However if this is a completely rubbish question then I think it should be deleted.

Angular: How to check change in queryParams before ngOnDestroy is called

I would like to conditionally execute some code in ngOnDestroy based on changes in current route.
Route is changed from /foo to /login?logout=true, and this change is triggered outside of Foo component.
In ngOnInit I am subscribing to queryParam changes, to update correctly loggingOut flag.
My problem is, that ngOnDestroy is called before next handler of queryParam, so the loggingOut has incorrect value.
export class FooComponent implements OnInit, OnDestroy {
loggingOut = false;
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
this.route.queryParamMap.subscribe(queryParams => {
this.loggingOut = queryParams.get('logout') === 'true';
});
}
ngOnDestroy(): void {
if (this.loggingOut) {
// do this
} else {
// do that
}
}
}
Seems this is intended behavior from lifecycle POV, so have following question:
Is there a way to check route changes before ngOnDestory is called?
If possible, please add link to documentation describing, how are lifecycle hooks (especially ngOnDestory) called with respect to navigation changes?
Thanks.
My problem is, that ngOnDestroy is called before next handler of queryParam
componentDestroyed = false;
ngOnInit(): void {
this.route.queryParamMap.subscribe(queryParams => {
if (!this.componentDestroyed)
this.loggingOut = queryParams.get('logout') === 'true';
else {
// Do here what you wanted to do in ngOnDestroy
}
});
}
ngOnDestroy(): void {
this.componentDestroyed = true;
}
Would this fix your problem?

Subscribing to Observable not triggering change detection

I am using 'angular2-virtual-scroll' to implement load on demand. The items used to be driven by observable's using the async pipe triggered by the parent component. Now i am trying to call my service from the child. The call is successful and i get my data, i need to use the subscribe event to apply other logic. The issue is change detected does not appear to be working when i update my arrays in the subscribe function. I have read other similar issues but i have had no luck finding a solution.
This is the main component where the service calls are used. The inital request is done from the onInit. And then when you scroll down fetchMore is called.
import { Component, OnInit, Input, OnDestroy } from '#angular/core';
import { Store } from '#ngrx/store';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { User } from './../models/user';
import { Role } from './../../roles/models/role';
import { UsersService } from './../services/users.service';
import { ChangeEvent } from 'angular2-virtual-scroll';
import { promise } from 'selenium-webdriver';
import { VirtualScrollComponent } from 'angular2-virtual-scroll';
import { Subscription } from 'rxjs/Subscription';
#Component({
selector: 'app-users-list',
template: `
<div class="status">
Showing <span class="">{{indices?.start + 1}}</span>
- <span class="">{{indices?.end}}</span>
of <span class="">{{users?.length}}</span>
<span>({{scrollItems?.length}} nodes)</span>
</div>
<virtual-scroll [childHeight]="75" [items]="users" (update)="scrollItems = $event" (end)="fetchMore($event)">
<div #container>
<app-user-info *ngFor="let user of scrollItems" [roles]="roles" [user]="user">
<li>
<a [routerLink]="['/users/edit/', user.id]" class="btn btn-action btn-edit">Edit</a>
</li>
</app-user-info>
<div *ngIf="loading" class="loader">Loading...</div>
</div>
</virtual-scroll>
`
})
export class UsersListComponent implements OnInit, OnDestroy {
users: User[] = [];
#Input() roles: Role[];
currentPage: number;
scrollItems: User[];
indices: ChangeEvent;
readonly bufferSize: number = 20;
loading: boolean;
userServiceSub: Subscription;
constructor(private usersService: UsersService) {
}
ngOnInit() {
this.reset();
}
ngOnDestroy() {
if(this.userServiceSub) {
this.userServiceSub.unsubscribe();
}
}
reset() {
this.loading=true;
this.currentPage = 1;
this.userServiceSub = this.usersService.getUsers(this.currentPage).subscribe(users => {
this.users = users;
});
}
fetchMore(event: ChangeEvent) {
if (event.end !== this.users.length) return;
this.loading=true;
this.currentPage += 1;
this.userServiceSub = this.usersService.getUsers(this.currentPage).subscribe(users => {
this.users = this.users.concat(users);
});
}
}
From what i have read this could be a context issue but i am not sure. Any suggestions would be great.
"EDIT"
Looking at the source code for the plugin component i can see where the change event is captured.
VirtualScrollComponent.prototype.ngOnChanges = function (changes) {
this.previousStart = undefined;
this.previousEnd = undefined;
var items = changes.items || {};
if (changes.items != undefined && items.previousValue == undefined || (items.previousValue != undefined && items.previousValue.length === 0)) {
this.startupLoop = true;
}
this.refresh();
};
If i put a breakpoint in this event it fires on the initial load, so when we instantiate the array to []. It fires when i click on the page. But it does not fire when the array is update in the subscribe event. I have even put a button in that sets the array to empty, and that updates the view so the subscribe function must be breaking the change detection.
So when you say the change detection does not appear to be working, I assume you are referring to this: *ngFor="let user of scrollItems"?
I have not used that particular component nor do I have any running code to work with ... but I'd start by taking a closer look at this:
<virtual-scroll [childHeight]="75"
[items]="currentBuffer"
(update)="scrollItems = $event"
(end)="fetchMore($event)">
Maybe change the (update) to call a method just to ensure it is emitting and that you are getting what you expect back from it.
EDIT:
Here is an example subscription that updates the primary bound property showing the data for my page:
movies: IMovie[];
getMovies(): void {
this.movieService.getMovies().subscribe(
(movies: IMovie[]) => {
this.movies = movies;
this.performFilter(null);
},
(error: any) => this.errorMessage = <any>error
);
}
The change detection works fine in this case. So there is most likely something else going on causing the issue you are seeing.
Note that your template does need to bind to the property for the change detection to work. In my example, I'm binding to the movies property. In your example, you'd need to bind to the users property.
So change detection was not firing. I had to use "ChangeDetectorRef" with the function "markForCheck" to get change detection to work correctly. I am not sure why so i definitely have some research to do.

How to access functions created in the Component outside of Angular2 using JavaScript [duplicate]

I am using a javascript Object that has a callback. Once the callback is fired I want to call a function inside an Angular2 component.
example
HTML file.
var run = new Hello('callbackfunction');
function callbackfunction(){
// how to call the function **runThisFunctionFromOutside**
}
<script>
System.config({
transpiler: 'typescript',
typescriptOptions: { emitDecoratorMetadata: true },
packages: {'js/app': {defaultExtension: 'ts'}}
});
System.import('js/app/main')
.then(null, console.error.bind(console));
</script>
My App.component.ts
import {Component NgZone} from 'angular2/core';
import {GameButtonsComponent} from './buttons/game-buttons.component';
#Component({
selector: 'my-app',
template: ' blblb'
})
export class AppComponent {
constructor(private _ngZone: NgZone){}
ngOnInit(){
calledFromOutside() {
this._ngZone.run(() => {
this.runThisFunctionFromOutside();
});
}
}
runThisFunctionFromOutside(){
console.log("run");
}
How can i call the function runThisFunctionFromOutside which is inside App.component.ts
I basically followed this answer, but I didn't want my "outside" code to know anything about NgZone. This is app.component.ts:
import {Component, NgZone, OnInit, OnDestroy} from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: 'app.component.html'
})
export class AppComponent implements OnInit, OnDestroy {
constructor(private ngZone: NgZone) {}
ngOnInit() {
window.my = window.my || {};
window.my.namespace = window.my.namespace || {};
window.my.namespace.publicFunc = this.publicFunc.bind(this);
}
ngOnDestroy() {
window.my.namespace.publicFunc = null;
}
publicFunc() {
this.ngZone.run(() => this.privateFunc());
}
privateFunc() {
// do private stuff
}
}
I also had to add a definition for TypeScript to extend the window object. I put this in typings.d.ts:
interface Window { my: any; }
Calling the function from the console is now as simple as:
my.namespace.publicFunc()
See also How do expose angular 2 methods publicly?
When the component is constucted make it assign itself to a global variable. Then you can reference it from there and call methods.
Don't forget to use zone.run(() => { ... }) so Angular gets notified about required change detection runs.
function callbackfunction(){
// window['angularComponentRef'] might not yet be set here though
window['angularComponent'].zone.run(() => {
runThisFunctionFromOutside();
});
}
constructor(private _ngZone: NgZone){
window['angularComponentRef'] = {component: this, zone: _ngZone};
}
ngOnDestroy() {
window.angularComponent = null;
}
Plunker example1
In the browser console you have to switch from <topframe> to plunkerPreviewTarget.... because Plunker executes the code in an iFrame. Then run
window['angularComponentRef'].zone.run(() => {window['angularComponentRef'].component.callFromOutside('1');})
or
window.angularComponentRef.zone.run(() => {window.angularComponentRef.componentFn('2');})
An alternative approach
would be to dispatch events outside Angular and listen to them in Angular like explained in Angular 2 - communication of typescript functions with external js libraries
Plunker example2 (from the comments)
Below is a solution.
function callbackfunction(){
window.angularComponent.runThisFunctionFromOutside();
}
<script>
System.config({
transpiler: 'typescript',
typescriptOptions: { emitDecoratorMetadata: true },
packages: {'js/app': {defaultExtension: 'ts'}}
});
System.import('js/app/main')
.then(null, console.error.bind(console));
</script>
My App.component.ts
import {Component NgZone} from 'angular2/core';
import {GameButtonsComponent} from './buttons/game-buttons.component';
#Component({
selector: 'my-app',
template: ' blblb'
})
export class AppComponent {
constructor(private _ngZone: NgZone){
window.angularComponent = {runThisFunctionFromOutside: this.runThisFunctionFromOutside, zone: _ngZone};
}
runThisFunctionFromOutside(){
console.log("run");
}
}
An other approach without using global variables is to use pass a control object and bind its properties to the variables and methods to expose.
export class MyComponentToControlFromOutside implements OnChanges {
#Input() // object to bind to internal methods
control: {
openDialog,
closeDialog
};
ngOnChanges() {
if (this.control) {
// bind control methods to internal methods
this.control.openDialog = this.internalOpenDialog.bind(this);
this.control.closeDialog = this.internalCloseDialog;
}
}
internalOpenDialog(): Observable<boolean> {
// ...
}
internalCloseDialog(result: boolean) {
// ...
}
}
export class MyHostComponent {
controlObject= {};
}
<my-component-to-control [control]="controlObject"></my-component-to-control>
<a (click)="controlObject.open()">Call open method</a>
I had a similar situation when using the callback 'eventClick' of the fullCalendar library, whose callbacks are returning from outside the angular zone, causing my application to have partial and unreliable effects. I was able to combine the zone approach and a closure reference to the component as seen below in order to raise an output event. Once I started executing the event inside of the zone.run() method the event and it's effects were once again predictable and picked up by angular change detection. Hope this helps someone.
constructor(public zone: NgZone) { // code removed for clarity
}
ngOnInit() {
this.configureCalendar();
}
private configureCalendar() {
// FullCalendar settings
this.uiConfig = {
calendar: { // code removed for clarity
}
};
this.uiConfig.calendar.eventClick = this.onEventClick();
}
private onEventClick() {
const vm = this;
return function (event, element, view) {
vm.zone.run(() => {
vm.onSequenceSelected.emit(event.sequenceSource);
});
return false;
};
}
Just adding to #Dave Kennedy:
Calling the function from the console is now as simple as:
my.namespace.publicFunc()
1) If we try to access our component's public method from a different domain you will get caught into CORS issue (the cross origin problem, can be solved if both server and client code resides in same machine).
2) if you were to call this method from server using javascript, you will have to use window.opener.my.namespace.publicFunc() instead of window.my.namespace.publicFunc():
window.opener.my.namespace.publicFunc();

how can i call a function when a template loads in angular2?

I am new to angular2. I have a requirement to call a function when a template loads/initializes. I know how to do this in angular1.x., but I am not able to find out how it can be done in angular-2.
This is how I tried in angular1.x
In html
<div ng-init="getItems()">
//some logic to get my items
</div>
In controller
getItems = function(){
console.log('in the getitems function call');
//logic to get my items from db/localStorage
}
This is how I used ng-init in angular1.x, but there is no ng-init in angular-2?Please help me on this issue.
#Component({
...
})
class MyComponent {
constructor() {
// when component class instance is created
}
ngOnChanges(...) {
// when inputs are updated
}
ngOnInit() {
// after `ngOnChanges()` was called the first time
}
ngAfterViewInit() {
// after the view was created
}
ngAfterContentInit() {
// after content was projected
}
}
See also https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html#!#hooks-overview for the full list
Check lifecycle events of a component https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html . From what you are saying you probably needs ngAfterViewInit
In angular2 you can use component phase ngOnInit it is equal to on-init in angularJS. Here is more information about lifecycle in angular.
Example:
export class PeekABoo implements OnInit {
constructor(private logger: LoggerService) { }
// implement OnInit's `ngOnInit` method
ngOnInit() {
this.logIt(`OnInit`);
}
protected logIt(msg: string) {
this.logger.log(`#${nextId++} ${msg}`);
}
}

Categories