Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions packages/angular/common/src/providers/platform.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { DOCUMENT } from '@angular/common';
import { NgZone, Inject, Injectable } from '@angular/core';
import type { DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { getPlatforms, isPlatform } from '@ionic/core/components';
import type { BackButtonEventDetail, KeyboardEventDetail, Platforms } from '@ionic/core/components';
import { Subscription, Subject } from 'rxjs';
Expand All @@ -9,7 +11,13 @@ import { Subscription, Subject } from 'rxjs';
export interface BackButtonEmitter extends Subject<BackButtonEventDetail> {
subscribeWithPriority(
priority: number,
callback: (processNextHandler: () => void) => Promise<any> | void
callback: (processNextHandler: () => void) => Promise<any> | void,
/**
* Pass a component's `DestroyRef` to have the subscription torn down with that
* component. Without it the subscription lives for the lifetime of the injector,
* which for a root-provided `Platform` means the lifetime of the application.
*/
Comment on lines +15 to +19

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing a DestroyRef ties this to ngOnDestroy timing, and with ion-router-outlet that only fires when a page is popped. Pushing from A to B leaves A in the DOM, so A's handler stays subscribed while B is on screen. That's the case your description opens with, and I don't think this would fix it.

What do you see when you push a few pages deep with this applied? I haven't run your app so I could be wrong about how it plays out in practice.

Our Angular lifecycle docs point people at ionViewWillLeave for unsubscribing for this reason. If that's the right hook, then "torn down with that component" is going to read as covering navigate-away when it doesn't.

destroyRef?: DestroyRef

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this move to @param tags in one block above the signature? That's the shape we use elsewhere, swipeGesture in the menu controller being the closest example, and it uses the [menuId] bracket form for the optional one.

I'd also trim the last two sentences. They explain why the change exists rather than what the parameter does, and they're already in the commit body and the PR description, so that's three copies to keep in sync. Up to you on that one.

): Subscription;
}

Expand Down Expand Up @@ -62,8 +70,10 @@ export class Platform {
constructor(@Inject(DOCUMENT) private doc: any, zone: NgZone) {
zone.run(() => {
this.win = doc.defaultView;
this.backButton.subscribeWithPriority = function (priority, callback) {
return this.subscribe((ev) => {
this.backButton.subscribeWithPriority = function (priority, callback, destroyRef) {
const source$ = destroyRef ? this.pipe(takeUntilDestroyed(destroyRef)) : this;

return source$.subscribe((ev) => {
return ev.register(priority, (processNextHandler) => zone.run(() => callback(processNextHandler)));
});
};
Comment on lines +73 to 79

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this.backButton.subscribeWithPriority = function (priority, callback, destroyRef) {
const source$ = destroyRef ? this.pipe(takeUntilDestroyed(destroyRef)) : this;
return source$.subscribe((ev) => {
return ev.register(priority, (processNextHandler) => zone.run(() => callback(processNextHandler)));
});
};
this.backButton.subscribeWithPriority = function (priority, callback, destroyRef) {
const subscription = this.subscribe((ev) => {
return ev.register(priority, (processNextHandler) => zone.run(() => callback(processNextHandler)));
});
destroyRef?.onDestroy(() => subscription.unsubscribe());
return subscription;
};

Using takeUntilDestroyed means depending on developer preview API. It's @developerPreview in Angular 16, still is in 18, and only becomes @publicApi in 19, so neither our >=16 floor here nor >=18 on major-9.0 gets the stable version. The DestroyRef type itself has been stable since 16 though.

The operator is only registering an onDestroy and using its unregister function as teardown, so doing that directly gets the same behavior and drops the @angular/core/rxjs-interop import, which would be our first runtime import from a secondary @angular/core entry point.

Worth a line in the docs either way: passing an already-destroyed DestroyRef will now throw, where before this method never threw at all.

Expand Down