I have Ionic 2 app with one view for 3 different data sets. Data are loaded in constructor and based on variable in page params, it's decided which data set to show.
At every successful data call by observable, event handler logs success when data are loaded. But this only works when I click/load view for a first time. If I click for 2nd or any other time, data are not re-loaded (no log). Also, when I just console log anything, it won't show at 2nd+ click.
So I wonder what should I change to load data everytime and how constructor works in this manner.
This is how my code looks like. Jsons are called from namesListProvider.
#Component({
templateUrl: '...',
})
export class ListOfNames {
...
private dataListAll: Array<any> = [];
private dataListFavourites: Array<any> = [];
private dataListDisliked: Array<any> = [];
constructor(private nav: NavController, ...) {
...
this.loadJsons();
console.log('whatever');
}
loadJsons(){
this.namesListProvider.getJsons()
.subscribe(
(data:any) => {
this.dataListFavourites = data[0],
this.dataListDisliked = data[1],
this.dataListAll = data[2]
if (this.actualList === 'mainList') {
this.listOfNames = this.dataListAll;
this.swipeLeftList = this.dataListDisliked;
this.swipeRightList = this.dataListFavourites;
}
else if (...) {
...
}
this.listSearchResults = this.listOfNames;
}, err => console.log('hey, error when loading names list - ' + err),
() => console.info('loading Jsons complete')
)
}
What you're looking for are the Lifecycle events from Ionic2 pages. So instead of using ngOnInit you can use some of the events that Ionic2 exposes:
Page Event Description
---------- -----------
ionViewLoaded Runs when the page has loaded. This event only happens once per page being created and added to the DOM. If a page leaves but is cached, then this event will not fire again on a subsequent viewing. The ionViewLoaded event is good place to put your setup code for the page.
ionViewWillEnter Runs when the page is about to enter and become the active page.
ionViewDidEnter Runs when the page has fully entered and is now the active page. This event will fire, whether it was the first load or a cached page.
ionViewWillLeave Runs when the page is about to leave and no longer be the active page.
ionViewDidLeave Runs when the page has finished leaving and is no longer the active page.
ionViewWillUnload Runs when the page is about to be destroyed and have its elements removed.
ionViewDidUnload Runs after the page has been destroyed and its elements have been removed.
In your case, you can use the ionViewWillEnter page event like this:
ionViewWillEnter {
// This will be executed every time the page is shown ...
this.loadJsons();
// ...
}
EDIT
If you're going to obtain the data to show in that page asynchronously, since you don't know how long would it take until the data is ready, I'd recommend you to use a loading popup so the user can we aware of something happening in the background (instead of showing a blank page for a few seconds until the data is loaded). You can easily add that behaviour to your code like this:
// Import the LoadingController
import { LoadingController, ...} from 'ionic/angular';
#Component({
templateUrl: '...',
})
export class ListOfNames {
...
private dataListAll: Array<any> = [];
private dataListFavourites: Array<any> = [];
private dataListDisliked: Array<any> = [];
// Create a property to be able to create it and dismiss it from different methods of the class
private loading: any;
constructor(private loadingCtrl: LoadingController, private nav: NavController, ...) {
...
this.loadJsons();
console.log('whatever');
}
ionViewWillEnter {
// This will be executed every time the page is shown ...
// Create the loading popup
this.loading = this.loadingCtrl.create({
content: 'Loading...'
});
// Show the popup
this.loading.present();
// Get the data
this.loadJsons();
// ...
}
loadJsons(){
this.namesListProvider.getJsons()
.subscribe(
(data:any) => {
this.dataListFavourites = data[0],
this.dataListDisliked = data[1],
this.dataListAll = data[2]
if (this.actualList === 'mainList') {
this.listOfNames = this.dataListAll;
this.swipeLeftList = this.dataListDisliked;
this.swipeRightList = this.dataListFavourites;
}
else if (...) {
...
}
this.listSearchResults = this.listOfNames;
}, err => console.log('hey, error when loading names list - ' + err),
() => {
// Dismiss the popup because data is ready
this.loading.dismiss();
console.info('loading Jsons complete')}
)
}
The solution is don't do this in the constructor, use ngOnInit() instead. Components are created only once, therefore the constructor will only be called when first created.
Your component class must implement the OnInit interface:
import { Component, OnInit } from '#angular/core';
#Component({
templateUrl: '...',
})
export class ListOfNames implements OnInit {
constructor(...)
ngOnInit() {
this.loadJsons();
}
private loadJsons() {
...
}
}
i'm coming from Angular 2 world, not ionic, but angular 2 has the option to register callbacks on init/destory (ngInit/ngDestory).
try to move initialization to ngInit, save subscription handler, and don't forget to unsubscribe it on destory.
i think your issue related to that you are not unsubscribing.. :\
Related
Here is my app.component.ts (excerpt) -
export class AppComponent {
_subscription;
constructor(private themeService: ThemeService){
themeService.getDefaultTheme();
this._subscription = themeService.themeChange.subscribe((value) => {
//Some code
});
}
}
and theme.service.ts (excerpt) -
export class ThemeService {
themeChange: Subject<boolean> = new Subject<boolean>();
getDefaultTheme(){
this.changeTheme(true);
}
changeTheme(val:boolean){
//Some code
this.themeChange.next(val);
}
}
As app-root is my root component, the constructor in app.component.ts is called shortly after the initial page load. The constructor calls getDefaultTheme() which causes the Subject in theme.service.ts to emit an event. I am subscribing to that event back in the this._subscription ... part.
In short, on the initial page load, getDefaultTheme() should be called and the subscription should be handled as well.
But when I load the page, the getDefaultTheme() method is called but the subscription is not handled. I do not get any error at the compile time as well as in the run time.
I delayed the execution of getDefaultTheme() like
setTimeout(function(){
themeService.getDefaultTheme();
}, 5000);
Now the subscription was handled. I suspect that the event is not ready to be subscribed at the page load. How can I solve this?
It looks like you're emitting your subject before you've registered the subscription in the constructor. Swap over the call to your subject to be after you've registered the subscription.
export class AppComponent {
_subscription;
constructor(private themeService: ThemeService){
this._subscription = themeService.themeChange.subscribe((value) => {
//Some code
});
// After Subscription is listening
themeService.getDefaultTheme();
}
}
Subscribers to Subject could only receive notifications pushed to it's source after the subscription. Instead you could use ReplaySubject with buffer 1. It can "hold/buffer" the current value pushed to it and emit it immediately to future subscribers.
export class ThemeService {
themeChange: ReplaySubject<boolean> = new ReplaySubject<boolean>(1);
getDefaultTheme(){
this.changeTheme(true);
}
changeTheme(val:boolean){
//Some code
this.themeChange.next(val);
}
}
While BehaviorSubject is also a viable alternative, it requires a default value during initialization:
themeChange: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
I have an app that saves items in an sqlite database and shows them in a list view.
It loads the listview with the correct data at the beginning of the app, but it doesn't refreshes the listview when I add a new item to the database.
This is the component.ts where I load the items to the observable
export class HomeComponent implements OnInit {
items: ObservableArray<IDataItem>;
constructor(public _itemService: DataService) {
}
ngOnInit(): void {
this.items = this._itemService.selectItems();
}
}
This is the DataService:
export class DataService {
private items = new ObservableArray<IDataItem>();
private database = new DatabaseService();
private db: any;
selectItems(): ObservableArray<IDataItem> {
this.database.getdbConnection()
.then(db => {
db.all("SELECT * FROM items").then(rows => {
for (let row in rows) {
this.items.push({ id: rows[row][0], sitioWeb: rows[row][1], usuario: rows[row][2], password: rows[row][3] });
}
this.db = db;
}, error => {
console.log("SELECT ERROR", error);
});
});
for(let i = 0; i < this.items.length; i++){
Toast.makeText(""+this.items[i].id+" "+this.items[i].sitioWeb+" "+this.items[i].usuario+" "+this.items[i].password,"10")
}
return this.items;
}
getItems(): ObservableArray<IDataItem> {
return this.items;
}
getItem(id: number): IDataItem {
return this.items.filter((item) => item.id === id)[0];
}
}
This is the view where the listview is located at:
<ActionBar class="action-bar">
<Label class="action-bar-title" text="Home"></Label>
</ActionBar>
<GridLayout class="page page-content" >
<ListView [items]="items | async" class="list-group" >
<ng-template let-item="item">
<Label [nsRouterLink]="['../item', item.id]" [text]="item.sitioWeb" class="list-group-item"></Label>
</ng-template>
</ListView>
</GridLayout>
It only loads items to the list view on the init of the app, but I want to refresh it when an item is added.
A simple solution would be to either fire a new request to get the list after you add a new item, or just add it in memory on the front end if you get a success response from the save request. However, while this would work, it's not a great solution. It wouldn't update the list when another user added an item.
I think you should look into using websockets for this. You can have a socket open listening for messages on the front end. The back end would emit a message every time something was added, even if it was added by another user. The front end listener would add that item to the list.
Here is a good tutorial using Sock.js and STOMP to implement websockets in angular.
https://g00glen00b.be/websockets-angular/
You can create sharer service with list of your item like this:
import { Injectable } from '#angular/core';
import { BehaviorSubject } from 'rxjs';
#Injectable()
export class DataService {
private messageSource = new BehaviorSubject('default message');
currentMessage = this.messageSource.asObservable();
constructor() { }
changeMessage(message: string) {
this.messageSource.next(message)
}
}
Where currentMessage is your list of items. Bind that list to you html and in callback function on create new item call method changeMessage witch will be add new created item in your array.
See this example
And not forget subscribe on changes in your list component.
Once you add a new item to the database you can call selectItems() service which will update the item list whenever new item is added or you can call selectItems() in ngAfterViewChecked() hook as it is executed every time the view of the given component has been checked by the change detection algorithm of Angular. This method executes after every subsequent execution of the ngAfterContentChecked()
I'm wondering if there's a way to pass data from a modal to the previous page without using nav controller. If I use that, in fact, I push another page on the stack, and I don't want this, I just want to go back to the existing one. The problem is that if I do:
this.viewCtrl.dismiss();
this.navCtrl.pop();
inside the modal when I want to close it, it gives me an error saying that there is no page to pop.
If I didn't need to pass the data I would simply use the dismiss() method for the modal, but I need to return some data to the previous page and I don't know how to do that. Is it possible? How can I achieve this?
Just like you can see here you can send some data in the dismiss method and get it in the previous page (you don't need to use this.navCtrl.pop();):
// Page A
// ----------
private dataFromModal: any;
presentModal() {
let modal = this.modalCtrl.create(ModalPage);
modal.onWillDismiss((data) => {
// This is going to be executed when the modal is closed, so
// you can get the data here
this.dataFromModal = data;
});
modal.present();
}
// Page B
// ----------
let data = { 'foo': 'bar' };
this.viewCtrl.dismiss(data);
You could store the data in the Storage and then on dismiss fetch the info like so:
import { Storage } from '#ionic/storage';
constructor(private navCtrl: NavController, private navParams: NavParams,
private modalCtrl: ModalController,
private storage: Storage) {}
openModal() {
let modal = this.modalCtrl.create(ModalPage);
modal.present();
modal.onDidDismiss(data => {
this.getInfo();
});
}
getInfo() {
this.storage.get("some_data").then(res => {
if(res != null){
this.data = res;
}
});
}
modal.ts
setInfo() {
this.storage.set('some_data', data);
this.dismiss();
}
So i have a Modal Component with a form in it, this component is used for both creating an entry to the DB and editing an existing one.
It has a subscription option to the onSubmit event, which is being executed on a successful submit.
What happens for some reason is that some of this component's element subscription executes and some won't, and it looks like those on the "create-mode" will and those on the "edit-mode" wont.
Code Section
CreateOrUpdateTransactionComponent:
#Component({
selector: 'create-update-transaction',
templateUrl: './CreateOrUpdateTransaction.html',
providers: [AccountTransactionsService]
})
export class CreateOrUpdateTransactionComponent {
closeResult: string;
modalRef: NgbModalRef;
#Input() transaction: Transaction = new Transaction();
#Input() isCreate: boolean;
#Output() onSubmit: EventEmitter<void> = new EventEmitter<void>();
constructor(private modalService: NgbModal,
private transactionsService: AccountTransactionsService) {}
sendTransaction(): void{
let localModalRef = this.modalRef;
this.transactionsService.createOrUpdateTransaction(this.transaction, (isSuccessful)=>{
if (isSuccessful) {
this.onSubmit.emit(); //<--- The problem is here
localModalRef.close();
}
});
}
}
The HTML:
<table>
<caption>Account Transactions</caption>
<thead>
// Omitted thead
</thead>
<template let-transaction ngFor [ngForOf]="accountTransactions" let-i="index">
<tr data-toggle="collapse" [attr.data-target]="'#'+i">
// Omitted <td>s
<td> //<----These updateTransactions() are not being executed
<create-update-transaction [isCreate]="false" [transaction]="transaction" (onSubmit)="updateTransactions()"></create-update-transaction>
</td>
</tr>
<div class="container collapse" [attr.id]="i">
// some content
</div>
</template>
</table>
<create-update-transaction [isCreate]="true" (onSubmit)="updateTransactions()"></create-update-transaction>
//<---- This updateTransactions() successfully executes
Notice
If I only display one row in the table not using ngFor (keeping the call to the back-end to update the DB), it works perfectly fine.
Any idea why would this happen?
Thanks in advance!
Update1
Debugging i could notice that when on the create-mode the this.onSubmit.observers is an array with one observer and on the edit-mode its an array with 0 observers, so thats the problem. any idea why?
Update2
Debugging again and found that the this in this.transactionsService.createOrUpdateTransaction... is fine and its' onSubmit.observers contains 1 observer, before reaching the callback's code, in which the this.onSubmit.observers is an array of 0 observers
AccountTransactionsService:
#Injectable()
export class AccountTransactionsService{
private loggedBankAccount: number;
private queryObservable: ObservableQuery;
constructor(private userManagingService: UserManagingService) {
this.loggedBankAccount = userManagingService.getLoggedBankAccountNumber();
this.queryAccountTransactions();
}
queryAccountTransactions(): void{
this.queryObservable = // querying the back-end
}
createOrUpdateTransaction(transaction: Transaction, callback: (isSuccessfull: boolean) => void): void{
let isSuccessful: boolean = false;
client.mutate(/*inserting the backend*/).then((graphQLResult) => {
const { errors, data } = graphQLResult;
if (data) {
console.log('got data', data);
isSuccessful = true;
}
if (errors) {
console.log('got some GraphQL execution errors', errors);
}
callback(isSuccessful);
}).catch((error) => {
console.log('there was an error sending the query', error);
callback(isSuccessful);
});
}
getAccountTransactions(): ObservableQuery{
return this.queryObservable;
}
}
Notice
If i just execute the callback give to the AccountTransactionService.createOrUpdateTransaction (removing the call to the back-end to actually update the DB) it works perfectly fine and all the subscribers to this onSubmit event are being called.
this Console image
Set the null as a parameter :
this.onSubmit.emit(null);
So I found out the case was that the data the ngFor is bound to was being replaced by a new instance as I updated the backend, hence, it rerendered it's child Components causing reinitializing (destory + init) of them, which made the instance of the Component to be overwritten.
In order to solve this issue i have changed the querying of the accountTransaction to be only one time querying, on initializing of the parent component, and never refetching again (which triggers the rerendering).
Im displaying to the client the changes only if they succeeded on the server side, and if they failed i use a backup of the data, so the client is kept update of the real state of the server WITHOUT refetching
For the future lookers to come:
The key to the problem was that the Parent Component's *ngFor depended on data that was changing in the Child Components, causing reinitializing of the *ngFor (the Child Components) BEFORE finishing executions of the Child Components methods.
Hope it'll be helpful to somebody :)
I have 2 pages Page1 and Page2. I have used this.nav.pop() in Page2 and it will pop the Page2 and Page1 will enable but i want to refresh the Page1.
Thank you in advance.
you could pass the parent page along with the nav push. that way you could accces the parent page as a navParamter.
in parent page:
goToChildPage() {
this.navCtrl.push(ChildPage, { "parentPage": this });
}
and in the child page before pop you could call functions on parent page
this.navParams.get("parentPage").someFnToUpdateParent();
//or
this.navParams.get("parentPage").someFnToRefreshParent();
Ignore the direct angular implementations suggested here, especially since you are using Ionic 2 and the suggestions are assuming Ionic 1. Don't start mixing too much of direct angular in your ionic app unless there is no ionic implementation for what you need. Import "Events" from ionic/angular2 in both Page1 and Page2, then in Page2 do something like
this.events.publish('reloadPage1');
this.nav.pop();
And in Page1 put
this.events.subscribe('reloadPage1',() => {
this.nav.pop();
this.nav.push(Page1);
});
You may want to implement one of these in your page:
ionViewWillEnter
ionViewDidEnter
Please review the navController and page lifecycle documentation:
http://ionicframework.com/docs/v2/api/components/nav/NavController/
Simple solution that worked for me was calling the get service method again in ionViewDidEnter
ionViewDidEnter() {
this.loadGetService();
}
On PAGE 1:
import { Events } from 'ionic-angular'
constructor(public events:Events){
this.listenEvents();
}
... ...
listenEvents(){
this.events.subscribe('reloadDetails',() => {
//call methods to refresh content
});
}
On PAGE 2:
import { Events } from 'ionic-angular'
constructor(public events:Events, public navCtrl:NavController){
}
function(){
this.events.publish('reloadDetails');
this.navCtrl.pop();
}
You may consider send an event before call this.nav.pop to let page 1 reload itself.
Like Jonathan said, you can import Events from ionic-angular, but you don't need push and pop again, call your methods to reload only the content.
In page2:
this.events.publish('reloadDetails');
this.navCtrl.pop();
In page1:
this.events.subscribe('reloadDetails',() => {
//call methods to refresh content
});
That works for me.
I simply load the details in page 1 in an ionViewWillEnter function (using Ionic 2). This handles both the initial load and any refresh when popping back to page 1.
Documentation is here.
ionViewWillEnter
"Runs when the page is about to enter and become the active page."
I found this technique to reload a page:
this.navCtrl.insert(1, MyPage);
this.navCtrl.pop();
I had the same problem and spend many hours searching and trying the solution.
If I understand, your problem is:
Page 1 have some bindings that you get from an API / Webservice.
Page 2 have some inputs and when pushing the back button (pop) you want to SAVE data + refresh the Page 1 bindings.
The way I solved it has been reading a post on StackOverflow that now I can't find :( !!
The solution is using an Injectable Service.
PAGE 1:
/* IMPORTS */
import { App, Nav, NavParams } from 'ionic-angular';
import { Oportunidades } from '../../services/oportunidades.service';
/* SOME BINDINGS HERE */
{{oportunidades.mesActual.num_testdrive}}
/* CONSTRUCTOR */
constructor(
private oportunidades: Oportunidades, // my injectable service!
public app: App,
public nav: Nav,
public params: NavParams
) {
// Call to the Injectable Service (named oportunidades):
this.oportunidades.getOportunidades();
}
INJECTABLE SERVICE:
/* IMPORTS */
import { Injectable } from '#angular/core';
import { Http } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
#Injectable()
export class Oportunidades {
public url = 'http://your-API.URL';
public data: Observable<Object>;
public mesActual: Object = [];
constructor(private http: Http){
//GET API DATA
this.data = http.get(this.url).map(res => res.json());
}
getOportunidades() {
this.data.subscribe(data => {
this.mesActual = new MesActual(
data["mes_actual_slide"].num_testdrive,
...
//Here I get the API data and set it on my injectable object
);
});
}
}
PAGE 2:
/* SOME IMPORTS */
import { NavController } from 'ionic-angular';
import { UserData } from '../../services/data.service';
import { Oportunidades } from '../../services/oportunidades.service';
import { Http, Headers, URLSearchParams } from '#angular/http';
/* SOME example BINDINGS and INPUTS: */
#Component({
template: `
{{ day[selectedDay].dia }}
Input data:
<ion-input type="number" clearOnEdit="true"
#ventas id="ventas" value={{day[selectedDay].ventas}}
(keyup)="setVal(ventas.value, $event)">
</ion-input>
`,
providers: [
]
})
export class PageInsert {
constructor(
public navCtrl: NavController,
private http: Http,
private userData: UserData,
public oportunidades: Oportunidades // my injectable service!
) {
send(selectedDay){
var url = 'http://your.api.url/senddata';
// I SAVE data TO API / Webservice
this.http.post(url, params, { headers: headers })
.map(res => res.json())
.subscribe(
data => {
console.log(data);
// Here i'll call to the Injectable service so It refresh's the new data on Page1
// in my case, this send function is called when "pop" or "back" button of ionic2 is pressed
// This means: On click on back button -> Save and refresh data of the Injectable that is binded with the Page1
this.oportunidades.getOportunidades();
return true; },
error => {
console.error("Error saving!");
}
);
}
}
I hope it can help you!! Ask for any similar problems :)
I spent a day and a half on a similar issue. The solution is anti-climatic really.
I'm passing a FormGroup from Page-1 to Page-2. I update the FormGroup values in Page-2. When I pop Page-2, Page-1's form is not updated with the new values. It hasn't been watching for changes.
To fix this, I patch the FormGroup with itself after Page-2 has been popped but still in the Page-2 component.
This is more responsive, but requires a direct call to close().
// Page-2 close method
public close(): void {
this.navCtrl.pop();
this.formGroup.patchValue(this.formGroup.value);
}
This is all encompassing, but I do see the refresh on Page-1.
// Page-2 nav controller page event method
ionViewWillUnload() {
this.formGroup.patchValue(this.formGroup.value);
}
In some situations instead of pop() you can use the push() function. When you enter the page with the push() function it is reloaded.
Also you can remove page2 from the navigation.
this.navCtrl.push(TabsPage).then(() => {
const index = this.viewCtrl.index;
this.navCtrl.remove(index);
});
Or if you have more than one page for example page1->page2->pag3:
this.navCtrl.push(TabsPage).then(() => {
const index = this.viewCtrl.index;
this.navCtrl.remove(index, 2); //this will remove page3 and page2
});
ionViewWillEnter() {
this.refresh();
}
ionViewWillEnter will be called just before any time you (re)visualize the page
Please checkout my solution, which I've posted here:
https://forum.ionicframework.com/t/ionviewdidenter-is-not-invoked-on-leaving-a-pushed-page/131144/19?u=unkn0wn0x
Maybe you can adapt it to your needs.
The main point of my intention was to prevent, passing a whole module with this as a navCtrlParam into the push()'ed page, like it was mentioned in some comments before.
Hope it helps!
Cheers
Unkn0wn0x