mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-18 19:21:34 +08:00
fix(angular): tabs supports conditional slot bindings (#27582)
Issue number: Resolves #19484 --------- <!-- 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. --> The Angular implementation of `ion-tabs` does not support conditional binding the `slot` property on `ion-tab-bar` or assigning a variable as the slot. For example, this usage is invalid: ```html <ion-tabs> <ion-tab-bar [slot]="slot"></ion-tab-bar> </ion-tabs> ``` This occurs because `ng-content` only supports static content projection. It is not intended for scenarios where the content can be relocated or conditionally rendered. An example of static content projection would be: ```html <ion-tabs> <ion-tab-bar slot="top"></ion-tab-bar> </ion-tabs> ``` ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - `ion-tabs` supports conditional slot bindings or a slot that is bound to a variable in Angular. The revised implementation relocates the tab bar in the `ion-tabs` template, based on it's current slot attribute. The implementation checks the tab bar slot whenever the content changes. ## 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 <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> Dev-build: `7.0.11-dev.11685631370.18980633` --------- Co-authored-by: Liam DeBeasi <liamdebeasi@users.noreply.github.com>
This commit is contained in:
@ -1,4 +1,16 @@
|
||||
import { Component, ContentChild, EventEmitter, HostListener, Output, ViewChild } from '@angular/core';
|
||||
import {
|
||||
AfterContentChecked,
|
||||
AfterContentInit,
|
||||
Component,
|
||||
ContentChild,
|
||||
ContentChildren,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Output,
|
||||
QueryList,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
|
||||
import { NavController } from '../../providers/nav-controller';
|
||||
import { IonTabBar } from '../proxies';
|
||||
@ -8,11 +20,13 @@ import { StackEvent } from './stack-utils';
|
||||
|
||||
@Component({
|
||||
selector: 'ion-tabs',
|
||||
template: `<ng-content select="[slot=top]"></ng-content>
|
||||
<div class="tabs-inner">
|
||||
template: `
|
||||
<ng-content select="[slot=top]"></ng-content>
|
||||
<div class="tabs-inner" #tabsInner>
|
||||
<ion-router-outlet #outlet tabs="true" (stackEvents)="onPageSelected($event)"></ion-router-outlet>
|
||||
</div>
|
||||
<ng-content></ng-content>`,
|
||||
<ng-content></ng-content>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
@ -41,15 +55,28 @@ import { StackEvent } from './stack-utils';
|
||||
],
|
||||
})
|
||||
// eslint-disable-next-line @angular-eslint/component-class-suffix
|
||||
export class IonTabs {
|
||||
export class IonTabs implements AfterContentInit, AfterContentChecked {
|
||||
@ViewChild('outlet', { read: IonRouterOutlet, static: false }) outlet: IonRouterOutlet;
|
||||
@ViewChild('tabsInner', { read: ElementRef, static: true }) tabsInner: ElementRef<HTMLDivElement>;
|
||||
|
||||
@ContentChild(IonTabBar, { static: false }) tabBar: IonTabBar | undefined;
|
||||
@ContentChildren(IonTabBar) tabBars: QueryList<IonTabBar>;
|
||||
|
||||
@Output() ionTabsWillChange = new EventEmitter<{ tab: string }>();
|
||||
@Output() ionTabsDidChange = new EventEmitter<{ tab: string }>();
|
||||
|
||||
private tabBarSlot = 'bottom';
|
||||
|
||||
constructor(private navCtrl: NavController) {}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this.detectSlotChanges();
|
||||
}
|
||||
|
||||
ngAfterContentChecked(): void {
|
||||
this.detectSlotChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -137,4 +164,48 @@ export class IonTabs {
|
||||
getSelected(): string | undefined {
|
||||
return this.outlet.getActiveStackId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects changes to the slot attribute of the tab bar.
|
||||
*
|
||||
* If the slot attribute has changed, then the tab bar
|
||||
* should be relocated to the new slot position.
|
||||
*/
|
||||
private detectSlotChanges(): void {
|
||||
this.tabBars.forEach((tabBar: any) => {
|
||||
// el is a protected attribute from the generated component wrapper
|
||||
const currentSlot = tabBar.el.getAttribute('slot');
|
||||
|
||||
if (currentSlot !== this.tabBarSlot) {
|
||||
this.tabBarSlot = currentSlot;
|
||||
this.relocateTabBar();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Relocates the tab bar to the new slot position.
|
||||
*/
|
||||
private relocateTabBar(): void {
|
||||
/**
|
||||
* `el` is a protected attribute from the generated component wrapper.
|
||||
* To avoid having to manually create the wrapper for tab bar, we
|
||||
* cast the tab bar to any and access the protected attribute.
|
||||
*/
|
||||
const tabBar = (this.tabBar as any).el as HTMLElement;
|
||||
|
||||
if (this.tabBarSlot === 'top') {
|
||||
/**
|
||||
* A tab bar with a slot of "top" should be inserted
|
||||
* at the top of the container.
|
||||
*/
|
||||
this.tabsInner.nativeElement.before(tabBar);
|
||||
} else {
|
||||
/**
|
||||
* A tab bar with a slot of "bottom" or without a slot
|
||||
* should be inserted at the end of the container.
|
||||
*/
|
||||
this.tabsInner.nativeElement.after(tabBar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -435,6 +435,30 @@ describe('Tabs', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('Tabs should support conditional slots', () => {
|
||||
cy.visit('/tabs-slots');
|
||||
|
||||
cy.get('ion-tabs .tabs-inner + ion-tab-bar').should('have.length', 1);
|
||||
|
||||
// Click the button to change the slot to the top
|
||||
cy.get('#set-slot-top').click();
|
||||
|
||||
// The tab bar should be removed from the bottom
|
||||
cy.get('ion-tabs .tabs-inner + ion-tab-bar').should('have.length', 0);
|
||||
|
||||
// The tab bar should be added to the top
|
||||
cy.get('ion-tabs ion-tab-bar + .tabs-inner').should('have.length', 1);
|
||||
|
||||
// Click the button to change the slot to the bottom
|
||||
cy.get('#set-slot-bottom').click();
|
||||
|
||||
// The tab bar should be removed from the top
|
||||
cy.get('ion-tabs ion-tab-bar + .tabs-inner').should('have.length', 0);
|
||||
|
||||
// The tab bar should be added to the bottom
|
||||
cy.get('ion-tabs .tabs-inner + ion-tab-bar').should('have.length', 1);
|
||||
});
|
||||
|
||||
|
||||
function testTabTitle(title) {
|
||||
const tab = getSelectedTab();
|
||||
|
@ -57,6 +57,10 @@ const routes: Routes = [
|
||||
path: 'tabs-global',
|
||||
loadChildren: () => import('./tabs-global/tabs-global.module').then(m => m.TabsGlobalModule)
|
||||
},
|
||||
{
|
||||
path: 'tabs-slots',
|
||||
loadComponent: () => import('./tabs-slots.component').then(c => c.TabsSlotsComponent)
|
||||
},
|
||||
{
|
||||
path: 'nested-outlet',
|
||||
component: NestedOutletComponent,
|
||||
|
32
angular/test/base/src/app/tabs-slots.component.ts
Normal file
32
angular/test/base/src/app/tabs-slots.component.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { IonicModule } from "@ionic/angular";
|
||||
|
||||
/**
|
||||
* Test purpose: Validates that the tab bar is relocated to the
|
||||
* correct location when the slot attribute changes or is bound
|
||||
* to a variable.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-tabs-slots',
|
||||
template: `
|
||||
<ion-tabs>
|
||||
<ion-tab-bar [slot]="slot"></ion-tab-bar>
|
||||
</ion-tabs>
|
||||
<ion-fab vertical="bottom" horizontal="end">
|
||||
<button id="set-slot-top" (click)="setSlot('top')">Slot - top</button>
|
||||
<br />
|
||||
<button id="set-slot-bottom"(click)="setSlot('bottom')">Slot - bottom</button>
|
||||
</ion-fab>
|
||||
`,
|
||||
imports: [IonicModule],
|
||||
standalone: true
|
||||
})
|
||||
export class TabsSlotsComponent {
|
||||
|
||||
slot?: string;
|
||||
|
||||
setSlot(newSlot: string) {
|
||||
this.slot = newSlot;
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user