ionViewWillEnter & cross-tab navigation

I’m interested in the “approved” way to implement the following. Just getting something to work is easy enough, but my method feels hacky.

Background: I have two tabs, “Quiz” and “History”. In the “Quiz” tab the user presses an “Ask” button. This pulls a question from a database and displays it. Then the user enters an answer and submits. Then the user presses “Ask” again, when they are ready for the next question, and this repeats. The “History” tab is simply an of of questions previously asked. The user can click there, see old questions, and go back to the quiz tab and continue to “Ask”. It works nicely.

Requirement: When the user clicks on a question in the History tab, I want to “replay” the question: simply navigate back to the Quiz tab and display that old question again, as if the “Ask” button was pressed and that question was generated from the database. This is different from just pressing the “quiz” tab button, which starts that tab from scratch. Here we are going back to the quiz tab but with a specific question “preloaded”.

Next, the code. This is the history.page.html:

 <ion-list>
   <ion-item *ngFor="let q of questions">
     <span (click)="replay(q)">
     {{q.text}}}      
     </span>
   </ion-item>
 </ion-list>

This is the history.page.ts with the replay() function that navigates back to the quiz page:

 replay(q: Question) {
   this.navCtrl.navigateForward(['/tabs/quiz/'], { state: {question: q} } );
 }

Now in the quiz.page.ts, I have

 ionViewWillEnter() {
   this.question = history.state.question;
 }

Everything works but it doesn’t feel clean somehow. Finally my questions.

  1. Is this the right way to pass a complex object? I can pass the question ID back and then pull the object again from the database service, but the code is so neat elegant if we just pass whole object. We have the object; we pass it. I just dislike retrieving it from that “history.state” global, that seems horrible somehow.

  2. What Angular lifecycle event to use instead of ionViewWillEnter()? My understanding was that the ion hooks are deprecated and we should switch to the Angular hooks. But hooks like ngOnInit() and ngAfterViewInit() are only called once while ngAfterContentChecked() and ngAfterViewChecked() are called way too many times on the same click. None that I tried seem to work as well as ionViewWillEnter().

  3. Finally, if I stick with ionViewWillEnter(), what is the best way to determine this is being entered via the the navigateForward() from the history tab, versus it is entering from the quiz tab at the beginning of the app? Again, I can implement something with some state variables, but was hoping for some “official” method to detect “entry” versus “re-entry with a parameter”.

Thanks!

When I first started writing web apps, lifecycle events were my life raft in a sea of uncertainty. I spent hours debug printing which ones got called when and in what order, frantically looking for some way to wrest control of the CPU back to its rightful owner: ME.

That didn’t go so well for me. See this post, which while it’s nominally about the precise combination you’re struggling with here (ionViewWillEnter and tabs), is more about deciding to accept and even embrace the limitations inherent in certain design decisions.

In this case, those design decisions surround lifecycle events and tabs. I now deal with lifecycle events only when they are the only way to work around other inherent limitations: for example, ngOnChanges is the first, if not only, place I can rely on @Input properties being properly bound. ngAfterViewInit is the first place I can rely on @ViewChild being populated.

Tabs are a seductive UI option, but very easily abused, and I think that’s happening here. I would argue that the reason you’re having trouble wiring up cross-tab interaction is tabs should be independent of one another. They’re literally modeled on old-school paper filing systems involving folders. Say you’re a lawyer, and you ask for the FrobozzCo file. It comes to your desk. You take out a bunch of papers, shuffle them around, add some more, throw some away, then you put the file back in the drawer. Nothing that you did to anything in the FrobozzCo folder affected the contents of any other folder.

With that groundwork having been laid, here’s how I would answer your questions:

  1. The “right” way to pass complex objects in my opinion is via Observables exposed by service providers that are injected wherever needed. Usually they are backed by Subjects, but that’s not required.

  2. None. This isn’t a lifecycle situation. I would have a service provider that looks like so:

class QuizService {
  activeQuestion$ = new BehaviorSubject<Question | undefined>(undefined);
  watchActiveQuestion(): Observable<Question | undefined> { return this.activeQuestion$; }
  peekActiveQuestion(): Question | undefined { return this.activeQuestion$.value; }
  pokeActiveQuestion(q: Question) { this.activeQuestion$.next(q); }
}

I would have my quiz component listen to watchActiveQuestion and react accordingly. As for the hook to call pokeActiveQuestion, …

  1. None of this matters, because I wouldn’t do this as tabs. I would make History and Quiz completely distinct pages. The click on your “old question” from the History page can call pokeActiveQuestion and (if desired) tell the router to move to the Quiz page.
1 Like

Thank you for the detailed and thoughtful answer.

First, I temporarily solved all my issues such as “re-entry detection” (#3), by doing this:

  ionViewWillEnter() {
    if (history.state.question !== undefined) {
      this.question = history.state.question;
    }
  }

This works perfectly. However, it is ugly, and after studying your examples and the previous article you wrote, I will rewrite it with observables and the async pipe. That’s simple enough. So I just have two responses:

  1. In addition to getting the question value for updating the view, I have to perform another actions. One is purely a side-effect and has nothing to do with the view: I speak the question aloud (using WebSpeechKit). In the code structure above that is trivial, but with observables, while having the view update is easy, figuring out how to do this other stuff on “re-entry” is tricky.

  2. I’m not sure what you have against tabs. Each tab is still a completely independent page, right? It just has that nice container page with the tab icons at the bottom, each of which routes to its corresponding page. So… I don’t understand your objection there. Isn’t it just a UI pattern?

Again, great suggestions and I will use them. I’m old-school enough to recognize (and enjoy!) peek and poke, but will probably rename those nevertheless :slight_smile:

Would enjoy reading any other comments you may care to add. Thanks again!

Figuring out when to speak the question is something I would do with lifecycle events (likely ionViewDidEnter), because it’s dependent on when the view is active, not on what goes on with the data. The notion of which question to speak I would manage with Observables.

I always go through the thought exercise with the manila file folders in the smoky detective’s office when it comes to tabs. In this case, we have one folder marked “quiz” and one marked “history”. We open the “quiz” folder, and we’re currently on question #8. We ask and answer question 8, and question #9 comes up. We decide not to answer #9 just yet, but instead close the “quiz” folder and toss it on our desk next to the half-full whiskey glass, planning to review our answers so far.

Grab the “history” folder and rifle through the pages until we find #3. Let’s take a closer look at that one for a moment. Here’s my problem: now the “quiz” folder is no longer the way we left it, ready for us to answer question #9. So no, I wouldn’t say the two views are independent.

To me, it’s a UI pattern that makes one very important promise: what happens in tab 2 stays in tab 2. There are three broad categories of problem where I think tabs are a great fit:

A. Something like a browser or file manager, where the user wants to walk around a space, leaving little cameras around to peek in on later. As long as the cameras are pointing at different things (web pages or folders), they’re totally independent.

B. Looking at one thing from many different perspectives. Imagine a personnel screening system where we have one tab for the candidate’s resume, one tab for reading through their published articles in various journals, and then another for a chat with them. Again, nothing that happens in any given tab will affect any other.

C. “Hive” apps where you really have several “mini-apps” packaged as one. Imagine a wellness management system with different tabs for diet, exercise, mental health.

So, no, I would not present this with a tab UI, because if I were a user, it would confuse and frustrate my expectations. I also realize that you may not agree with this opinion, but it’s presented (a) to try to convince you to agree on “moral” grounds, along with (b) explanation that if you do restrict yourself to these situations, you don’t have to worry about what Ionic chooses to do with respect to lifecycle events and tabs - specifically the fact that ionViewWillEnter isn’t guaranteed to fire when you switch tabs.

All that being said, if you’re still reading, you can fake something that looks like tabs using segments, but internally is implemented differently, and lifecycle events should be more intuitive.

1 Like

All right, that makes sense. It is very doable and will look much cleaner.

As for the tab paradigm, you are once again highly persuasive (darn you). Yes, if the user is on the quiz tab pondering the new question #9, switches to history and replays question #3, this takes him back to quiz, overwriting question #9 with question #3. That is fine but, if the tab structure at the bottom made him think his question #9 was still waiting safely back in the quiz tab, yes, he may be mad once he realizes it’s gone.

I will refactor the UI, tiresome as that will be. Many thanks for the valuable guidance, both on coding and on principles.

1 Like