I have a component in which i subscribe to an Observable. In the callback i set member variables in that component. Those variables’ values will then be displayed on the component page.
But, when the app goes out of scope (e.g. by manually doing so or by the Android OS-Popup asking for location permission) and comes back in, the view is not updated anymore (though the callback still receives new values as the console log proves).
A working example would a blank starter app with the following content of the homepage class (requires the cordova geolocation plugin)
export class HomePage implements OnInit {
lat = 0;
long = 0;
private subscription: Subscription;
constructor(private geolocation: Geolocation,
private zone: NgZone)
{
}
ngOnInit()
{
console.log('constructor() Subscribing');
this.renewSubscription();
}
renewSubscription()
{
if(this.subscription)
{
this.subscription.unsubscribe();
this.subscription = null;
}
this.subscription = this.geolocation
.watchPosition({ enableHighAccuracy : true})
.subscribe(this.onLocationChange);
}
private onLocationChange = (location: Geoposition) =>
{
if(location.coords)
{
console.log(location.coords.latitude + ':' + location.coords.longitude);
this.lat = location.coords.latitude;
this.long = location.coords.longitude;
}
else
{
console.dir('no coordinates');
console.dir(location);
}
}
}
and the following as a replacement for the Home page html content
{{ lat }}:{{ long }}.
As a bit of searching, i suspect that after the app resumption many of the the code does not live in the angular zone anymore or something like this, because when setting lat and long inside the zone explicitly it works again
this.zone.run(() => {
this.lat = location.coords.latitude;
this.long = location.coords.longitude;
});
The thing is that this is a very over-simplified version of the app i'm working on - which has a lot of subscriptions and indirections, so i was not able to find the code i have to put inside the angular zone manually.
Is there a way to run the app's code within the angular zone after app resumption just like it would after a normal app start?
EDIT
Putting the code into platform.resume won't work either.
Related
I am building a site what lets you build a time table (for school subjects) I wrote a component that fetches data from database and then calculates corect placement in time table
<div class="timetable col-lg-8">
<div class="rowT">
<div style="width : 16%">Day</div>
<div *ngFor="let head of timeTableHeader"
[style.width]="headerWidth">
{{head}}
</div>
</div>
<div *ngFor="let sublists of allData" class="rowT">
<div *ngFor="let sublist of sublists"
id="{{sublist[0]}}"
class="timetable-cell"
[style]="getMyStyles(sublist[1])"
(click)="showDetails($event,sublist[3])">
{{sublist[0]}}
</div>
</div>
now I wrote a form which allows somebody to edit particular subject(e.g. time changed or class room) it works fine it saves the changes in DB adn now I want to show these changes in my view I thought I just call the function that calculates subject placements in time table but that results in rendering the time table again and leaving the old one there.
#Component({
selector: 'time-table',
templateUrl: './timetable.component.html',
styleUrls: ['./timetable.component.css']
})
export class TimeTableComponent {
allSubjects: Array<Subject>;
headerWidth: string;
timeTableHeader: Array<string> = new Array();
allData: Array<Array<Array<string>>> = new Array<Array<Array<string>>>();
constructor(private http: Http) {
this.fetchAndMake(); //function that fetches data and calls other function
//to make calculations... too long and not sure if relevant
// so I wont post it here
}
fetchAndMake(){
this.allSubjects = new Array<Subject>();
let params : URLSearchParams = new URLSearchParams();
params.set('userName', this.authService.currentUser.userName);
let reqOption = new RequestOptions();
reqOption.params = params;
this.http.get(this.configurations.baseUrl + "/SubjectModel/TimeTable", reqOption).subscribe(result => {
this.makeSubjects(result.json());
});
}
updateSubject(subj){
let subject = subj as SubjectData;
this.http.post(this.configurations.baseUrl + "/SubjectModel/UpdateSubject",helper)
.subscribe();
this.editSubjectView = false;
this.fetchAndMake();
}
}
Thanks in advance for the help.
First you should not be fetching the data directly from the component but rather from a data service. When you use a data service and inject it into components that use it the data service is a singleton. Not sure if that solves your problem but in any case it is a design issue you should look into before you go any further down this road.
Also, you are calling the primary function in the constructor. The only thing you should be doing in the constructor is to inject the data service. You need to implement the OnInit interface and call your function that fetches the data from the data service you injected in the constructor in the ngOnInit() method.
Check out some of the many Angular 4 tutorials and look for examples of creating a data service, making it a provider in app.module.ts. Then in your component
import { Component, OnInit } from '#angular/core';
import { MyDataService } from '../shared/my-data.service';
....
export class TimeTableComponent Implements OnInit {
...
constructor(private _mydata: MyDataService) { }
ngOnInit(){
// call method in data service that fetches data and do whatever you need to
// with the returned result. Now everything will be available to your view
}
There is a chance that
this.fetchAndMake()
gets called before
this.http.post(this.configurations.baseUrl +
"/SubjectModel/UpdateSubject",helper)
.subscribe();
is complete in updateSubject function. Subscribe just initiates the call , if you want to ensure that the new data is updated only after the post is complete, edit the updateSubject() function as follows :-
updateSubject(subj){
let subject = subj as SubjectData;
this.http.post(this.configurations.baseUrl + "/SubjectModel/UpdateSubject",helper)
.subscribe(result =>{
this.allData = new Array<Array<Array<string>>>();
this.fetchAndMake();
});
this.editSubjectView = false;
}
I hope all of you are Ok :)
I am developing a simple application in Ionic Framework with Google Maps (Map + Places SearchBox). The problem is that all works good until I try to search an address (i. e. Morena Blvd #1, San Diego, CA), the input is showing me the suggested addresses but when I choose one of them It never completes the text in my <ion-input> element...
It works very good in my browser when I run my app with ionic serve but not in my iPhone or Android device. Have you experienced any issue like that?
Here is my code (frontend):
<ion-content no-padding>
<ion-searchbar #searchAddress id="searchAddress" [showCancelButton]="false" [(ngModel)]="address">
</ion-searchbar>
And this is my code to set the SearchBox instance to my ion-searchbar inner input control...
#ViewChild('searchAddress', {read: ElementRef}) searchElement: ElementRef;
...
...
...
...
let input = this.searchElement.nativeElement.querySelector('.searchbar-input');
let searchBox = new google.maps.places.SearchBox(input);
searchBox.addListener('places_changed', () => {
let places = searchBox.getPlaces();
if(!places && places.length === 0) return;
this.address = places[0].formatted_address;
this.latitude = places[0].geometry.location.lat();
this.longitude = places[0].geometry.location.lng();
if(places[0].address_components.length === 0) {
this.street = places[0].address_components[1].long_name;
this.selectedEstate = places[0].address_components[4].long_name;
}
});
And it's running in my ionViewDidEnter() event
Try wrapping everything inside your listener in an NgZone:
import { NgZone } from '#angular/core';
export class myPage {
zone: NgZone;
constructor() {
this.zone = new NgZone({enableLongStackTrace: false});
}
...
searchBox.addListener('places_changed', () => {
this.zone.run(() => {
let places = searchBox.getPlaces();
if(!places && places.length === 0) return;
this.address = places[0].formatted_address;
this.latitude = places[0].geometry.location.lat();
this.longitude = places[0].geometry.location.lng();
if(places[0].address_components.length === 0) {
this.street = places[0].address_components[1].long_name;
this.selectedEstate = places[0].address_components[4].long_name;
}
});
});
}
If that works then it means that your listener-callback is executed outside the angular context, which means it does not trigger change detection and doesn't update the value. I had a similar problem once (only mobile phones affected) so it might be related to performance or efficiency optimizations to save battery life.
I am building a site using Angular 2. I have a detail page where I access it by passing the id in the url (http://localhost:3000/#/projectdetail/1). I try to access a service to get the correct project by the id that I pass through. This project is saved in a project variable, but the variable is actually undefined all the time.
This are the errors I get when I go to the detail page:
This are the code pieces that I use (if you need more code just ask):
The projectdetail.page.html:
<div class="container-fluid">
<h1>{{project.name}}</h1>
<div>{{project.description}}</div>
</div>
The projectdetail.page.ts:
public project: Project;
constructor(private projectsService: ProjectsService, private route: ActivatedRoute) {
for (let i = 0; i < 4; i++) {
this.addSlide();
}
}
ngOnInit() {
this.route.params.map(params => params['id']).subscribe((id) => {
this.projectsService.getProject(id).then(project => this.project = project);
});
}
The projects.service.ts:
getProject(id: number): Promise<Project> {
return this.http.get(this.url).toPromise().then(x => x.json().data.filter(project => project.id === id)[0]).catch(this.handleError);
}
The error you're getting is from the template.
At the time of template render, project is not available yet.
Try to wrap it in an *ngIf to avoid this.
<div class="container-fluid" *ngIf="project">
<h1>{{project.name}}</h1>
<div>{{project.description}}</div>
</div>
Template rendering is happening before you are getting data,
what you need is to create an empty object for project property,
option 1:
public project: Project = new Project (); // Provided its a class.
option 2:
If you may not instantiate Project (is an interface), you may use nullable properties
{{project.name}} -> {{project?.name}}
Hope this helps!!
I'm upgrading/rewriting an existing angular app to use angular2. My problem is that I want to open a OAuth flow in a new pop up window and once the OAuth flow is completed use window.postMessage to communicate back to the angular 2 app that the OAuth flow was successful.
Currently what I have is in the angular 2 service is
export class ApiService {
constructor(private _loggedInService: LoggedInService) {
window.addEventListener('message', this.onPostMessage, false);
}
startOAuthFlow() {
var options = 'left=100,top=10,width=400,height=500';
window.open('http://site/connect-auth', , options);
}
onPostMessage(event) {
if(event.data.status === "200") {
// Use an EventEmitter to notify the other components that user logged in
this._loggedInService.Stream.emit(null);
}
}
}
This template that is loaded at the end of the OAuth flow
<html>
<head>
<title>OAuth callback</title>
<script>
var POST_ORIGIN_URI = 'localhost:8000';
var message = {"status": "200", "jwt":"2"};
window.opener.postMessage(message, POST_ORIGIN_URI);
window.close();
</script>
</head>
</html>
Using window.addEventListener like this seems to completely break the angular 2 app, dereferencing this.
So my question is can I use window.addEventListener or should I not use postMessage to communicate back to the angular2 app?
** Complete angular2 noob so any help is appreciated
I have a complete Angular2 OAuth2 skeleton application on Github that you can refer to.
It makes use of an Auth service for OAuth2 Implicit grants that in turn uses a Window service to create the popup window. It then monitors that window for the access token on the URL.
You can access the demo OAuth2 Angular code (with Webpack) here.
Here is the login routine from the Auth service, which will give you an idea of what's going on without having to look at the entire project. I've added a few extra comments in there for you.
public doLogin() {
var loopCount = this.loopCount;
this.windowHandle = this.windows.createWindow(this.oAuthTokenUrl, 'OAuth2 Login');
this.intervalId = setInterval(() => {
if (loopCount-- < 0) { // if we get below 0, it's a timeout and we close the window
clearInterval(this.intervalId);
this.emitAuthStatus(false);
this.windowHandle.close();
} else { // otherwise we check the URL of the window
var href:string;
try {
href = this.windowHandle.location.href;
} catch (e) {
//console.log('Error:', e);
}
if (href != null) { // if the URL is not null
var re = /access_token=(.*)/;
var found = href.match(re);
if (found) { // and if the URL has an access token then process the URL for access token and expiration time
console.log("Callback URL:", href);
clearInterval(this.intervalId);
var parsed = this.parse(href.substr(this.oAuthCallbackUrl.length + 1));
var expiresSeconds = Number(parsed.expires_in) || 1800;
this.token = parsed.access_token;
if (this.token) {
this.authenticated = true;
}
this.startExpiresTimer(expiresSeconds);
this.expires = new Date();
this.expires = this.expires.setSeconds(this.expires.getSeconds() + expiresSeconds);
this.windowHandle.close();
this.emitAuthStatus(true);
this.fetchUserInfo();
}
}
}
}, this.intervalLength);
}
Feel free to ask if you have any questions or problems getting the app up and running.
So with a bit of investigation found out the problem. I was de-referencing this. This github wiki helped me understand it a bit more.
To solve it for my case needed to do a couple of things. Firstly I created a service that encapsulated the adding of an eventListener
import {BrowserDomAdapter} from 'angular2/platform/browser';
export class PostMessageService {
dom = new BrowserDomAdapter();
addPostMessageListener(fn: EventListener): void {
this.dom.getGlobalEventTarget('window').addEventListener('message', fn,false)
}
}
Then using this addPostMessageListener I can attach a function in my other service to fire
constructor(public _postMessageService: PostMessageService,
public _router: Router) {
// Set up a Post Message Listener
this._postMessageService.addPostMessageListener((event) =>
this.onPostMessage(event)); // This is the important as it means I keep the reference to this
}
Then it works how I expected keeping the reference to this
I think this is the Angular2 way:
(Dart code but TS should be quite similar)
#Injectable()
class SomeService {
DomAdapter dom;
SomeService(this.dom) {
dom.getGlobalEventTarget('window').addEventListener("message", fn, false);
}
}
I fiddled around with this for ages but in the end, the most robust way for me was to redirect the user to the oath page
window.location.href = '/auth/logintwitter';
do the oath dance in the backend (I used express) and then redirect back to a receiving front end page...
res.redirect(`/#/account/twitterReturn?userName=${userName}&token=${token}`);
There are some idiosyncracies to my solution because e.g. I wanted to use only JsonWebToken on the client regardless of login type, but if you are interested, whole solution is here.
https://github.com/JavascriptMick/learntree.org
I have a pretty simple Fluxible store:
export default class NoteStore extends BaseStore {
static storeName = "NoteStore";
static handlers = {
[Actions.NEW_NOTES_FETCHED]: 'handleNewNotes'
};
constructor(dispatcher) {
super(dispatcher);
this.notes = [];
}
handleNewNotes(notes) {
this.notes = [];
var i;
for (i = 0; i < notes.length; i++){
this.notes.push(notes[i].content);
}
this.emitChange();
}
/* ... */
dehydrate() {
return { notes: this.notes };
}
rehydrate(state) {
this.notes = state.notes;
}
// Shouldn't be necessary to override?
shouldDehydrate() {
return true;
}
}
NEW_NOTES_FETCHED is dispatched by an action that gets data from my backend API, and the store listens for that event and pulls in the data from the payload. As far as I can tell, all this is working because everything runs perfectly when running in the client.
The problem I'm having is that the NoteStore doesn't seem to be getting dehydrated when the server calls app.dehydrate(). I look at the JSON embedded into the page and I don't see my store anywhere, though I do see information for the RouteStore.
I registerd my store with the FluxibleContext, but do I need to do something additionally to add it to the dehydrate chain?
App bootstrapping code if relevant:
const app = new Fluxible({ component: Root });
const AppRouteStore = RouteStore.withStaticRoutes(routes);
app.registerStore(AppRouteStore); // AppRouteStore appears in dehydrated JSON
app.registerStore(HtmlHeadStore); // Neither of these do, though HtmlHeadStore doesn't need to
app.registerStore(NoteStore);
export default app;
Ok, I figured out what was wrong. Basically, the action that was supposed to dispatch the NEW_NOTES_FETCHED event wasn't returning a promise and so the logic of handling the response from the backend server was never actually run, even though the request itself was made and I saw it appear on the backend's logs.
I was about to tear my hair out puzzling over this for so long, so hopefully someone can learn from my struggle!