navController.push appears to push page but does not display it

In my Ionic app, I have a List page that lists Alerts. This page also has a “Create Alert” button.

  • Tapping on the list items opens a detail page displaying the details of that Alert.
  • The button click should open the same detail page, but a blank one, in which the user can enter data to create an Alert and Save.

Tapping on list item is working fine, that is, it opens up the detail page and displays the contents of the Alert object I am passing over.

However, clicking on the “Create Alert” button does not open the same page. Nothing happens, no error. It does appear to overlap the same list page (or a blank one or something else) on top of itself. If I click the Back button it slides something. I think I know the reason why it is not loading the detail page. It’s because the Alert object that I use on the detail page is NULL/Undefined in scenario when the detail page is opened for creating an Alert. Now, because in the HTML I am referring to that object in [(ngModel)] in all the components (input, select, etc.) and the object is NULL it is failing to load the page.

Would appreciate if somebody can confirm if I am doing it right or please suggest the right way to do it. Maybe, I need to use FormBuilder, FormControl and FormGroup. I have not used it till now so was trying to avoid it. Will do it if that’s the right way. :slight_smile: Or perhaps that’s the only way to do it in my scenario.

Please help.

I am on MacOS with iPhone 6 Plus as device and latest version of everything.

Thank you!

Here is the code:

List Page HTML

<ion-header>
  <ion-navbar color='navbarColor'>
    <ion-title>Alert List</ion-title>
    <ion-buttons end>
      <button ion-button (click)="refresh()">
        <i class="fa fa-refresh fa-2x" aria-hidden="true"></i>
      </button>
    </ion-buttons>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <button block ion-button (click)="createAlert()">
    Create Alert
  </button>

  <ion-list>
    <ion-item detail-push *ngFor="let alert of alerts" (click)="editAlert(alert)">
      <h2><b>Name: {{alert.profileName}}</b></h2>
      <p>Type: {{alert.alertTypeName}}</p>
    </ion-item>
  </ion-list>
</ion-content>

List Page TypeScript

// All Imports here...

@Component({
  selector: 'page-alert-list',
  templateUrl: 'alert-list.page.html'
})
export class AlertListPage {
  loading: Loading;
  title = "Navbar Title";

  alerts: AlertSubscriptionInfo[];

  constructor(public navCtrl: NavController, public navParams: NavParams,
    private loadingCtrl: LoadingController, public alertService: AlertService,
    private alertCtrl: AlertController, private toastCtrl: ToastController, 
    private events: Events) {

  }

  ionViewDidLoad() {
    console.log('AlertListPage->ionViewDidLoad().');
    this.getAlertList();
  }

  getAlertList() {
    this.showLoading();
    this.alertService.getAlerts().then(
      (res) => {
        setTimeout(() => {
          this.loading.dismiss();
          this.alertSubscriptions = res.result;
        });
      },
      (error) => {
        this.showError(error);
      }
    );
  }

  refresh() {
    this.getAlertList();
  }

  createAlert() {
    this.navCtrl.push(AlertDetailPage, { "title": 'Add Alert' });
  }

  editAlert(alert: AlertSubscriptionInfo) {
    this.navCtrl.push(AlertDetailPage, { "title": 'Edit Alert', "alert": alert });
  }

  showLoading() {
    this.loading = this.loadingCtrl.create({
      content: 'Please wait...'
    });
    this.loading.present();
  }

  showError(text) {
    setTimeout(() => {
      this.loading.dismiss();
    });

    let alert = this.alertCtrl.create({
      title: 'Error',
      subTitle: text,
      buttons: ['OK']
    });
    alert.present();
  }
}

Detail Page HTML

<ion-header>
  <ion-navbar color='navbarColor'>
    <ion-title>{{title}}</ion-title>
    <ion-buttons end>
      <button (click)="saveAlert()" color="blue" ion-button icon-only>
        Done
      </button>
    </ion-buttons>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <ion-item>
    <ion-label stacked>Alert Profile Name</ion-label>
    <ion-input [(ngModel)]="currentAlert.profileName" value={{currentAlert.profileName}}></ion-input>
  </ion-item>
  <ion-item>
    <ion-label>Alert Type</ion-label>
    <ion-select [(ngModel)]="currentAlert.alertTypeId">
      <ion-option *ngFor="let alertType of alertTypes" value="{{alertType.alertTypeId}}">
        {{alertType.alertName}}
      </ion-option>
    </ion-select>
  </ion-item>
  <ion-item>
    <ion-label>Alert Level</ion-label>
    <ion-select [(ngModel)]="selectedAlertLevel" (ionChange)="alertLevelChanged();">
      <ion-option *ngFor="let alertLevel of alertLevels" value="{{alertLevel}}">
        {{alertLevel.level}}
      </ion-option>
    </ion-select>
  </ion-item>
  <button block ion-button (click)="openHierarchyPage()" [disabled]="disableHierarchyButton">
    Add Hierarchy
  </button>
  <ion-item *ngIf="currentAlert.hierarchy.length > 0" >{{currentAlert.hierarchy.length}} store(s) selected.</ion-item>
  <ion-item-divider color="light"><b>Subscription Mode</b></ion-item-divider>
  <ion-item>
    <ion-label>Push Notification</ion-label>
    <ion-toggle [(ngModel)]="currentAlert.enabled" checked={{currentAlert.enabled}}></ion-toggle>
  </ion-item>

  <button block ion-button icon-start outline color="danger" *ngIf="alertSubscriptionId" (click)="deleteSubscription(alertSubscriptionId)">
    <i class="fa fa-trash fa-2x" aria-hidden="true"></i>
    Delete Alert
  </button>
</ion-content>

Detail Page TypeScript

// All Imports here...

@Component({
  selector: 'page-alert-add',
  templateUrl: 'alert-add.page.html'
})
export class AlertAddPage {
  loading: Loading;
  title: string;

  currentAlert: AlertSubscriptionInfo;
  alertTypes: AlertTypeInfo[];
  alertLevels: AlertLevelInfo[];
  selectedAlertLevel: AlertLevelInfo;
  disableHierarchyButton: boolean;

  constructor(public navCtrl: NavController, public navParams: NavParams,
    private loadingCtrl: LoadingController, private alertCtrl: AlertController,
    public alertSettingsService: AlertSettingsService, private events: Events) {

      this.currentAlert = this.navParams.get('alert');
      this.title = this.navParams.get('title');
  }

  ionViewDidLoad() {
    console.log('In AlertAddPage->ionViewDidLoad().');
    this.showLoading();

    this.alertSettingsService.getAlertTypes().then(
      (res) => {
        this.alertTypes = res.result;
      },
      (err) => {
        this.showError(err);
      }
    );

    this.alertSettingsService.getLevels().then(
      (res) => {
        this.loading.dismiss();
        this.alertLevels = res.result;
      },
      (err) => {
        this.showError(err);
      }
    );

    this.checkToDisableHierarchyButton();
  }

  //TODO: Temporary, use a Service Provider class instead by injecting it in the pages that needs data back-and-forth
  myCallbackFunction = (_params) => {
    return new Promise((resolve, reject) => {
        this.currentAlert = _params;
        resolve();
    });
  }

  validateAlert(): boolean {
    if (this.currentAlert.profileName == null) {
      this.showValidationMessage('Alert Profile Name', 'Please enter a valid Alert Profile Name.');
      return false;
    }
    else if (this.currentAlert.alertTypeId == null) {
      this.showValidationMessage('Alert Type', 'Please select an Alert Type.');
      return false;
    }
    else if (this.currentAlert.levelOrder == null) {
      this.showValidationMessage('Level', 'Please select an Alert Level.');
      return false;
    }
    else if (this.currentAlert.hierarchy.length == 0) {
      this.showValidationMessage('Hierarchy', 'Please select Hierarchy Level(s).');
      return false;
    }

    return true;
  }

  saveAlert() {
    if(this.validateAlert()) {
      this.showLoading();
      
      this.alertSettingsService.saveAlert(this.currentAlert)
        .then(
          (httpreq) => {
            setTimeout(() => {
              this.loading.dismiss();
              this.events.publish('alertCreate:success');
              this.navCtrl.pop();
            });
          },
          (error) => {
            this.showError(error);
          }
        );
      }
  }

  showValidationMessage(titleStr: string, msg: string): void {
    let alert = this.alertCtrl.create({
      title: titleStr,
      message: msg,
      buttons: [
        {
          text: 'Ok',
          role: 'OK',
          handler: () => {
            console.log('OK Clicked.');
          }
        }
      ]
    });

    alert.present();
  }

  checkToDisableHierarchyButton(): void {
    if(this.currentAlert.levelOrder > 0) {
      this.disableHierarchyButton = false;
    }
    else {
      this.disableHierarchyButton = true;
    }
  }

  openHierarchyPage() {
    this.navCtrl.push(AlertStoresPage, { "alert": this.currentAlert, 'callback': this.myCallbackFunction });
  }

  public alertLevelChanged(): void {
    this.currentAlert.levelOrder = this.selectedAlertLevel.levelOrder;
    this.currentAlert.levelName = this.selectedAlertLevel.level;
    this.checkToDisableHierarchyButton();
  }

  deleteSubscription(alertSubscriptionId: number) {
    this.showLoading();
    this.alertSettingsService.deleteScubscription(alertSubscriptionId)
      .subscribe(
      (result) => {
        setTimeout(() => {
          this.loading.dismiss();
          if (result) {
            this.events.publish('alertDelete:success');
            this.navCtrl.pop();
          }
          else {
            this.showError('Server Error. Please try later. Or contact your Technical Support.')
          }
        });
      },
      (error) => {
        this.showError(error);
      }
      );
  }

  showLoading() {
    this.loading = this.loadingCtrl.create({
      content: 'Please wait...'
    });
    this.loading.present();
  }

  showError(text) {
    setTimeout(() => {
      this.loading.dismiss();
    });

    let alert = this.alertCtrl.create({
      title: 'Error',
      subTitle: text,
      buttons: ['OK']
    });
    alert.present();
  }
}

In my opinion, any array property that is referenced in a template should be initialized at the point of declaration to [].

1 Like

Thanks for the feedback @rapropos! That’s a good point.

In my case, the arrays are really getting set at the beginning so I think that will not be causing the issue I am facing.

Any inputs on what will be the correct approach to implement this functionality?

Thank you, appreciated!

Not according to the code you have posted, they aren’t. You are referencing alertLevels from the template, and it is not initialized:

// no initializer here:
alertLevels: AlertLevelInfo[];

Fundamentally, I disagree with the entire notion of having service providers dealing with view layer concerns like alerts, but that is something that reasonable people can agree to disagree on. I also think it is a mistake to have that loading property, because it encourages reuse and double-dispose bugs. I always make loading components lexically scoped.

Perhaps I worded it wrong. alertLevels is getting set in ionViewDidLoad() and I am pretty sure it is not the reason of my issue. But I will try initializing it as you said and see if that resolves my issue. Will post here soon.

As for other points, you mentioned it aptly “but that is something that reasonable people can agree to disagree on.” :slight_smile:
But I have noted them and might bring the last one into practice.

Your spinner is suspicious, and I wonder if its iffy dismissal interferes with the responsiveness of your button. If you put the button in the footer, do you still have the problem? This might be an issue with an area of your screen, instead of the button itself.

If that’s what’s going on, I can talk about better code style.

Again, not according to the code you posted. It’s being set in a then block, which by definition does not execute during ionViewDidLoad() itself.

Aah okay, now I get it, @rapropos ! Now I understand why you were saying that.

So when you say not initialized, it meant that it will not necessarily get set in ionViewDidLoad() right away since it’s a Promise. It will get set as and when the Promise returns successfully. Therefore, if Promise is taking time to return, the template will try to use an uninitialized variable and therefore fail.

Sorry, I was slow to understand. Will try it again by initializing the array with ‘[ ]’.

I had one more question. Am I suppose to use Angular Forms (ReactiveFormsModule, FormBuilder, FormGroup and FormControl) in this? Or what I am doing in the code is right?
Basically, I have a alert list page and a alert detail page. And I want to use the detail page for creating a new alert as well. Not sure, what is the right approach.

Thanks!

Right. This is an extremely common problem, and I have found the best way to avoid it is to just initialize everything right where it is defined, so you can be assured at a glance that the template will never see undefined.

Matter of preference. I tend to use reactive forms when validation is involved, and not when it isn’t.

One subtle thing I don’t like that may or may not bite you is having both [(ngModel)] and (ionChange). In general, one of my design rules is akin to the apochryphal Confucian saying about the man with two watches never being sure what time it is: “everything needs only one owner”. If you really need a change handler, I would take the banana out of the box and go with [ngModel]="foo" (ionChange)="fooChanged($event)" and update foo manually in fooChanged(). The concrete problem that bit me once here was if the change handler modifies the two-way bound property, change detection pukes and throws a “changed after checked” error.

Additionally, a couple of things about this line:

<ion-toggle [(ngModel)]="currentAlert.enabled" checked={{currentAlert.enabled}}></ion-toggle>

Prefer [foo]="bar" over foo="{{bar}}". Rationale here. In this case, you should get rid of checked entirely. It’s not harmful, but it is trumped by the presence of [(ngModel)], and having it in there might lure readers into believing it was doing something.

100% agree with @rapropos

Thanks for your kind inputs @rapropos, highly appreciated!