Skip to content

Commit fa78676

Browse files
authored
fix(angular): do not create duplicate menuController instances (#28343)
Issue number: resolves #28337 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> Duplicate instances of `menuController` are being created in `@ionic/angular`. `ion-menu` registers itself in the `menuController` from `@ionic/core`, but the `MenuController` from `@ionic/angular` uses the `menuController` from `@ionic/core/components`. This is how the overlay providers work too. Normally, this is not a problem. However, `menuController` caches references to registered menus in each controller instances: https://github.com/ionic-team/ionic-framework/blob/dcbf45101f4c0dc3541cd4c19186daa3766a6ce7/core/src/utils/menu-controller/index.ts#L14 This means that since there are two different controllers, `menuController` B does not know about the menus in `menuController` A. The end result is that the menu controller used in developer applications did not have references to the registered menus, which gave the impression that the menu controller did not work. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - Updated the architecture of `MenuController` in Ionic Angular to accept a `menuController` instance. This allows `@ionic/angular` to pass the `menuController` from `@ionic/core` and for `@ionic/angular/standalone` to pass the `menuController` from `@ionic/core/components`. Note: Overlay controllers don't **need** this change per-se since they don't cache references to overlays internally (they just query the DOM). However, I think it would be good to have a consistent architecture here, so I'll put up a separate PR that makes this change for overlays too. ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> ## Other information Dev build: `7.5.1-dev.11697123035.1ee6b4a2` <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. -->
1 parent 1ba9973 commit fa78676

File tree

14 files changed

+157
-63
lines changed

14 files changed

+157
-63
lines changed

core/src/components/menu/menu-interface.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Animation } from '../../interface';
1+
import type { Animation, AnimationBuilder } from '@utils/animation/animation-interface';
22

33
export type Side = 'start' | 'end';
44

@@ -26,13 +26,23 @@ export interface MenuI {
2626
}
2727

2828
export interface MenuControllerI {
29+
registerAnimation(name: string, animation: AnimationBuilder): void;
30+
get(menu?: string | null, logOnMultipleSideMenus?: boolean): Promise<HTMLIonMenuElement | undefined>;
31+
getMenus(): Promise<HTMLIonMenuElement[]>;
32+
getOpen(): Promise<HTMLIonMenuElement | undefined>;
33+
isEnabled(menu?: string | null): Promise<boolean>;
34+
swipeGesture(shouldEnable: boolean, menu?: string | null): Promise<HTMLIonMenuElement | undefined>;
35+
isAnimating(): Promise<boolean>;
36+
isOpen(menu?: string | null): Promise<boolean>;
37+
enable(shouldEnable: boolean, menu?: string | null): Promise<HTMLIonMenuElement | undefined>;
38+
toggle(menu?: string | null): Promise<boolean>;
39+
close(menu?: string | null): Promise<boolean>;
40+
open(menu?: string | null): Promise<boolean>;
41+
_getOpenSync(): HTMLIonMenuElement | undefined;
2942
_createAnimation(type: string, menuCmp: MenuI): Promise<Animation>;
30-
_setOpen(menu: MenuI, shouldOpen: boolean, animated: boolean): Promise<boolean>;
3143
_register(menu: MenuI): void;
3244
_unregister(menu: MenuI): void;
33-
34-
getMenus(): Promise<HTMLIonMenuElement[]>;
35-
getOpenSync(): HTMLIonMenuElement | undefined;
45+
_setOpen(menu: MenuI, shouldOpen: boolean, animated: boolean): Promise<boolean>;
3646
}
3747

3848
export interface MenuChangeEventDetail {

core/src/utils/menu-controller/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { printIonWarning } from '@utils/logging';
22

3-
import type { MenuI } from '../../components/menu/menu-interface';
3+
import type { MenuI, MenuControllerI } from '../../components/menu/menu-interface';
44
import type { AnimationBuilder, BackButtonEvent } from '../../interface';
55
import { MENU_BACK_BUTTON_PRIORITY } from '../hardware-back-button';
66
import { componentOnReady } from '../helpers';
@@ -9,7 +9,7 @@ import { menuOverlayAnimation } from './animations/overlay';
99
import { menuPushAnimation } from './animations/push';
1010
import { menuRevealAnimation } from './animations/reveal';
1111

12-
const createMenuController = () => {
12+
const createMenuController = (): MenuControllerI => {
1313
const menuAnimations = new Map<string, AnimationBuilder>();
1414
const menus: MenuI[] = [];
1515

packages/angular/common/src/providers/menu-controller.ts

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,15 @@
1-
import { Injectable } from '@angular/core';
2-
import { menuController } from '@ionic/core/components';
1+
import type { MenuControllerI } from '@ionic/core/components';
32

4-
@Injectable({
5-
providedIn: 'root',
6-
})
73
export class MenuController {
4+
constructor(private menuController: MenuControllerI) {}
5+
86
/**
97
* Programmatically open the Menu.
108
* @param [menuId] Optionally get the menu by its id, or side.
119
* @return returns a promise when the menu is fully opened
1210
*/
1311
open(menuId?: string): Promise<boolean> {
14-
return menuController.open(menuId);
12+
return this.menuController.open(menuId);
1513
}
1614

1715
/**
@@ -22,7 +20,7 @@ export class MenuController {
2220
* @return returns a promise when the menu is fully closed
2321
*/
2422
close(menuId?: string): Promise<boolean> {
25-
return menuController.close(menuId);
23+
return this.menuController.close(menuId);
2624
}
2725

2826
/**
@@ -32,7 +30,7 @@ export class MenuController {
3230
* @return returns a promise when the menu has been toggled
3331
*/
3432
toggle(menuId?: string): Promise<boolean> {
35-
return menuController.toggle(menuId);
33+
return this.menuController.toggle(menuId);
3634
}
3735

3836
/**
@@ -44,7 +42,7 @@ export class MenuController {
4442
* @return Returns the instance of the menu, which is useful for chaining.
4543
*/
4644
enable(shouldEnable: boolean, menuId?: string): Promise<HTMLIonMenuElement | undefined> {
47-
return menuController.enable(shouldEnable, menuId);
45+
return this.menuController.enable(shouldEnable, menuId);
4846
}
4947

5048
/**
@@ -54,7 +52,7 @@ export class MenuController {
5452
* @return Returns the instance of the menu, which is useful for chaining.
5553
*/
5654
swipeGesture(shouldEnable: boolean, menuId?: string): Promise<HTMLIonMenuElement | undefined> {
57-
return menuController.swipeGesture(shouldEnable, menuId);
55+
return this.menuController.swipeGesture(shouldEnable, menuId);
5856
}
5957

6058
/**
@@ -63,15 +61,15 @@ export class MenuController {
6361
* If the menuId is not specified, it returns true if ANY menu is currenly open.
6462
*/
6563
isOpen(menuId?: string): Promise<boolean> {
66-
return menuController.isOpen(menuId);
64+
return this.menuController.isOpen(menuId);
6765
}
6866

6967
/**
7068
* @param [menuId] Optionally get the menu by its id, or side.
7169
* @return Returns true if the menu is currently enabled, otherwise false.
7270
*/
7371
isEnabled(menuId?: string): Promise<boolean> {
74-
return menuController.isEnabled(menuId);
72+
return this.menuController.isEnabled(menuId);
7573
}
7674

7775
/**
@@ -84,20 +82,20 @@ export class MenuController {
8482
* @return Returns the instance of the menu if found, otherwise `null`.
8583
*/
8684
get(menuId?: string): Promise<HTMLIonMenuElement | undefined> {
87-
return menuController.get(menuId);
85+
return this.menuController.get(menuId);
8886
}
8987

9088
/**
9189
* @return Returns the instance of the menu already opened, otherwise `null`.
9290
*/
9391
getOpen(): Promise<HTMLIonMenuElement | undefined> {
94-
return menuController.getOpen();
92+
return this.menuController.getOpen();
9593
}
9694

9795
/**
9896
* @return Returns an array of all menu instances.
9997
*/
10098
getMenus(): Promise<HTMLIonMenuElement[]> {
101-
return menuController.getMenus();
99+
return this.menuController.getMenus();
102100
}
103101
}

packages/angular/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ export {
2323
ActionSheetController,
2424
AlertController,
2525
LoadingController,
26-
MenuController,
2726
ModalController,
2827
PickerController,
2928
PopoverController,
@@ -42,6 +41,7 @@ export {
4241
ViewDidEnter,
4342
ViewDidLeave,
4443
} from '@ionic/angular/common';
44+
export { MenuController } from './providers/menu-controller';
4545

4646
// PACKAGE MODULE
4747
export { IonicModule } from './ionic-module';
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Injectable } from '@angular/core';
2+
import { MenuController as MenuControllerBase } from '@ionic/angular/common';
3+
import { menuController } from '@ionic/core';
4+
5+
@Injectable({
6+
providedIn: 'root',
7+
})
8+
export class MenuController extends MenuControllerBase {
9+
constructor() {
10+
super(menuController);
11+
}
12+
}

packages/angular/standalone/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ export { IonRouterOutlet } from './navigation/router-outlet';
55
export { IonRouterLink, IonRouterLinkWithHref } from './navigation/router-link-delegate';
66
export { IonTabs } from './navigation/tabs';
77
export { provideIonicAngular } from './providers/ionic-angular';
8+
export { MenuController } from './providers/menu-controller';
89
export {
910
ActionSheetController,
1011
AlertController,
1112
LoadingController,
12-
MenuController,
1313
ModalController,
1414
PickerController,
1515
PopoverController,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Injectable } from '@angular/core';
2+
import { MenuController as MenuControllerBase } from '@ionic/angular/common';
3+
import { menuController } from '@ionic/core/components';
4+
5+
@Injectable({
6+
providedIn: 'root',
7+
})
8+
export class MenuController extends MenuControllerBase {
9+
constructor() {
10+
super(menuController);
11+
}
12+
}

packages/angular/test/base/e2e/src/lazy/providers.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,11 @@ describe('Providers', () => {
2727

2828
cy.get('#query-params').should('have.text', 'firstParam: abc, firstParam: true');
2929
})
30+
31+
// https://github.com/ionic-team/ionic-framework/issues/28337
32+
it('should register menus correctly', () => {
33+
cy.get('#set-menu-count').click();
34+
cy.get('#registered-menu-count').should('have.text', '1');
35+
});
3036
});
3137

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
describe('Menu Controller', () => {
2+
beforeEach(() => {
3+
cy.visit('/standalone/menu-controller');
4+
})
5+
6+
// https://github.com/ionic-team/ionic-framework/issues/28337
7+
it('should register menus correctly', () => {
8+
cy.get('#set-menu-count').click();
9+
cy.get('#registered-menu-count').should('have.text', '1');
10+
});
11+
})
Lines changed: 47 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,48 @@
1+
<ion-menu menu-id="menu" content-id="content">
2+
Menu Content
3+
</ion-menu>
4+
15
<ion-header>
2-
<ion-toolbar>
3-
<ion-title>
4-
Providers Test
5-
</ion-title>
6-
</ion-toolbar>
7-
</ion-header>
8-
<ion-content class="ion-padding">
9-
<p>
10-
isLoaded: <span id="is-loaded">{{isLoaded}}</span>
11-
</p>
12-
<p>
13-
isReady: <span id="is-ready">{{isReady}}</span>
14-
</p>
15-
<p>
16-
isResumed: <span id="is-resumed">{{isResumed}}</span>
17-
</p>
18-
<p>
19-
isPaused: <span id="is-paused">{{isPaused}}</span>
20-
</p>
21-
<p>
22-
isResized: <span id="is-resized">{{isResized}}</span>
23-
</p>
24-
<p>
25-
isTesting: <span id="is-testing">{{isTesting}}</span>
26-
</p>
27-
<p>
28-
isDesktop: <span id="is-desktop">{{isDesktop}}</span>
29-
</p>
30-
<p>
31-
isMobile: <span id="is-mobile">{{isMobile}}</span>
32-
</p>
33-
<p>
34-
keyboardHeight: <span id="keyboard-height">{{keyboardHeight}}</span>
35-
</p>
36-
<p>
37-
queryParams: <span id="query-params">{{queryParams}}</span>
38-
</p>
39-
</ion-content>
6+
<ion-toolbar>
7+
<ion-title>
8+
Providers Test
9+
</ion-title>
10+
</ion-toolbar>
11+
</ion-header>
12+
<ion-content class="ion-padding" id="content">
13+
<p>
14+
isLoaded: <span id="is-loaded">{{isLoaded}}</span>
15+
</p>
16+
<p>
17+
isReady: <span id="is-ready">{{isReady}}</span>
18+
</p>
19+
<p>
20+
isResumed: <span id="is-resumed">{{isResumed}}</span>
21+
</p>
22+
<p>
23+
isPaused: <span id="is-paused">{{isPaused}}</span>
24+
</p>
25+
<p>
26+
isResized: <span id="is-resized">{{isResized}}</span>
27+
</p>
28+
<p>
29+
isTesting: <span id="is-testing">{{isTesting}}</span>
30+
</p>
31+
<p>
32+
isDesktop: <span id="is-desktop">{{isDesktop}}</span>
33+
</p>
34+
<p>
35+
isMobile: <span id="is-mobile">{{isMobile}}</span>
36+
</p>
37+
<p>
38+
keyboardHeight: <span id="keyboard-height">{{keyboardHeight}}</span>
39+
</p>
40+
<p>
41+
queryParams: <span id="query-params">{{queryParams}}</span>
42+
</p>
43+
<p>
44+
Registered Menu Count: <span id="registered-menu-count">{{registeredMenuCount}}</span>
45+
</p>
46+
47+
<button id="set-menu-count" (click)="setMenuCount()">Set Menu Count</button>
48+
</ion-content>

0 commit comments

Comments
 (0)