mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
Compare commits
36 Commits
next
...
ld/icon-te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccd503d2fb | ||
|
|
4c87a06858 | ||
|
|
b7273c4613 | ||
|
|
dcbbc36bfd | ||
|
|
be9743475c | ||
|
|
be8e5ba1b3 | ||
|
|
fac5a97262 | ||
|
|
cfbc6e9952 | ||
|
|
a6e4fc4feb | ||
|
|
3f03299847 | ||
|
|
14eb906352 | ||
|
|
b2562e7c6a | ||
|
|
28f2ec9c62 | ||
|
|
dad7e66cf6 | ||
|
|
28deb56e9c | ||
|
|
3720ae6a09 | ||
|
|
01c34738a6 | ||
|
|
fd0f25aa72 | ||
|
|
0afa14ed7a | ||
|
|
e5b7f5ff55 | ||
|
|
e57759f4b6 | ||
|
|
ca53682684 | ||
|
|
c65e08dcd1 | ||
|
|
84212acce4 | ||
|
|
104b9547e5 | ||
|
|
e44a02632d | ||
|
|
c52a0972b9 | ||
|
|
8a97e406a0 | ||
|
|
37acdf9711 | ||
|
|
9f20780d66 | ||
|
|
3ea7488b47 | ||
|
|
de48493a16 | ||
|
|
5b31439ca0 | ||
|
|
291b1310a6 | ||
|
|
af68808e69 | ||
|
|
4553425502 |
@@ -7,6 +7,71 @@ import { vueOutputTarget } from '@stencil/vue-output-target';
|
||||
// @ts-ignore
|
||||
import { apiSpecGenerator } from './scripts/api-spec-generator';
|
||||
|
||||
const componentCorePackage = '@ionic/core';
|
||||
|
||||
const getAngularOutputTargets = () => {
|
||||
const excludeComponents = [
|
||||
// overlays that accept user components
|
||||
'ion-modal',
|
||||
'ion-popover',
|
||||
|
||||
// navigation
|
||||
'ion-router',
|
||||
'ion-route',
|
||||
'ion-route-redirect',
|
||||
'ion-router-link',
|
||||
'ion-router-outlet',
|
||||
'ion-nav',
|
||||
'ion-back-button',
|
||||
|
||||
// tabs
|
||||
'ion-tabs',
|
||||
'ion-tab',
|
||||
|
||||
// auxiliar
|
||||
'ion-picker-column',
|
||||
]
|
||||
return [
|
||||
angularOutputTarget({
|
||||
componentCorePackage,
|
||||
directivesProxyFile: '../packages/angular/src/directives/proxies.ts',
|
||||
directivesArrayFile: '../packages/angular/src/directives/proxies-list.ts',
|
||||
excludeComponents,
|
||||
outputType: 'component',
|
||||
}),
|
||||
angularOutputTarget({
|
||||
componentCorePackage,
|
||||
directivesProxyFile: '../packages/angular/standalone/src/directives/proxies.ts',
|
||||
excludeComponents: [
|
||||
...excludeComponents,
|
||||
/**
|
||||
* IonIcon is a special case because it does not come
|
||||
* from the `@ionic/core` package, so generating proxies that
|
||||
* are reliant on the CE build will reference the wrong
|
||||
* import location.
|
||||
*/
|
||||
'ion-icon',
|
||||
/**
|
||||
* Value Accessors are manually implemented in the `@ionic/angular/standalone` package.
|
||||
*/
|
||||
'ion-input',
|
||||
'ion-textarea',
|
||||
'ion-searchbar',
|
||||
'ion-datetime',
|
||||
'ion-radio',
|
||||
'ion-segment',
|
||||
'ion-checkbox',
|
||||
'ion-toggle',
|
||||
'ion-range',
|
||||
'ion-radio-group',
|
||||
'ion-select'
|
||||
|
||||
],
|
||||
outputType: 'standalone',
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
export const config: Config = {
|
||||
autoprefixCss: true,
|
||||
sourceMap: false,
|
||||
@@ -61,7 +126,7 @@ export const config: Config = {
|
||||
],
|
||||
outputTargets: [
|
||||
reactOutputTarget({
|
||||
componentCorePackage: '@ionic/core',
|
||||
componentCorePackage,
|
||||
includeImportCustomElements: true,
|
||||
includePolyfills: false,
|
||||
includeDefineCustomElements: false,
|
||||
@@ -98,7 +163,7 @@ export const config: Config = {
|
||||
]
|
||||
}),
|
||||
vueOutputTarget({
|
||||
componentCorePackage: '@ionic/core',
|
||||
componentCorePackage,
|
||||
includeImportCustomElements: true,
|
||||
includePolyfills: false,
|
||||
includeDefineCustomElements: false,
|
||||
@@ -182,30 +247,7 @@ export const config: Config = {
|
||||
// type: 'stats',
|
||||
// file: 'stats.json'
|
||||
// },
|
||||
angularOutputTarget({
|
||||
componentCorePackage: '@ionic/core',
|
||||
directivesProxyFile: '../packages/angular/src/directives/proxies.ts',
|
||||
directivesArrayFile: '../packages/angular/src/directives/proxies-list.ts',
|
||||
excludeComponents: [
|
||||
// overlays that accept user components
|
||||
'ion-modal',
|
||||
'ion-popover',
|
||||
|
||||
// navigation
|
||||
'ion-router',
|
||||
'ion-route',
|
||||
'ion-route-redirect',
|
||||
'ion-router-link',
|
||||
'ion-router-outlet',
|
||||
|
||||
// tabs
|
||||
'ion-tabs',
|
||||
'ion-tab',
|
||||
|
||||
// auxiliar
|
||||
'ion-picker-column',
|
||||
],
|
||||
}),
|
||||
...getAngularOutputTargets(),
|
||||
],
|
||||
buildEs5: 'prod',
|
||||
testing: {
|
||||
|
||||
@@ -52,3 +52,23 @@ $ npx schematics @ionic/angular:ng-add
|
||||
|
||||
|
||||
You'll now be able to add ionic components to a vanilla Angular app setup.
|
||||
|
||||
## Project Structure
|
||||
|
||||
**common**
|
||||
|
||||
This is where logic that is shared between lazy loaded and standalone components live. For example, the lazy loaded IonPopover and standalone IonPopover components extend from a base IonPopover implementation that exists in this directory.
|
||||
|
||||
**Note:** This directory exposes internal APIs and is only accessed in the `standalone` and `src` submodules. Ionic developers should never import directly from `@ionic/angular/common`. Instead, they should import from `@ionic/angular` or `@ionic/angular/standalone`.
|
||||
|
||||
**standalone**
|
||||
|
||||
This is where the standalone component implementations live. It was added as a separate entry point to avoid any lazy loaded logic from accidentally being pulled in to the final build. Having a separate directory allows the lazy loaded implementation to remain accessible from `@ionic/angular` for backwards compatibility.
|
||||
|
||||
Ionic developers can access this by importing from `@ionic/angular/standalone`.
|
||||
|
||||
**src**
|
||||
|
||||
This is where the lazy loaded component implementations live.
|
||||
|
||||
Ionic developers can access this by importing from `@ionic/angular`.
|
||||
|
||||
5
packages/angular/common/ng-package.json
Normal file
5
packages/angular/common/ng-package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"lib": {
|
||||
"entryFile": "src/index.ts"
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './value-accessor';
|
||||
@@ -2,7 +2,7 @@ import { AfterViewInit, ElementRef, Injector, OnDestroy, Directive, HostListener
|
||||
import { ControlValueAccessor, NgControl } from '@angular/forms';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { raf } from '../../util/util';
|
||||
import { raf } from '../../utils/util';
|
||||
|
||||
// TODO(FW-2827): types
|
||||
|
||||
@@ -17,11 +17,11 @@ export class ValueAccessor implements ControlValueAccessor, AfterViewInit, OnDes
|
||||
protected lastValue: any;
|
||||
private statusChanges?: Subscription;
|
||||
|
||||
constructor(protected injector: Injector, protected el: ElementRef) {}
|
||||
constructor(protected injector: Injector, protected elementRef: ElementRef) {}
|
||||
|
||||
writeValue(value: any): void {
|
||||
this.el.nativeElement.value = this.lastValue = value;
|
||||
setIonicClasses(this.el);
|
||||
this.elementRef.nativeElement.value = this.lastValue = value;
|
||||
setIonicClasses(this.elementRef);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,20 +38,20 @@ export class ValueAccessor implements ControlValueAccessor, AfterViewInit, OnDes
|
||||
* @param value The new value of the control.
|
||||
*/
|
||||
handleValueChange(el: HTMLElement, value: any): void {
|
||||
if (el === this.el.nativeElement) {
|
||||
if (el === this.elementRef.nativeElement) {
|
||||
if (value !== this.lastValue) {
|
||||
this.lastValue = value;
|
||||
this.onChange(value);
|
||||
}
|
||||
setIonicClasses(this.el);
|
||||
setIonicClasses(this.elementRef);
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('ionBlur', ['$event.target'])
|
||||
_handleBlurEvent(el: any): void {
|
||||
if (el === this.el.nativeElement) {
|
||||
if (el === this.elementRef.nativeElement) {
|
||||
this.onTouched();
|
||||
setIonicClasses(this.el);
|
||||
setIonicClasses(this.elementRef);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ export class ValueAccessor implements ControlValueAccessor, AfterViewInit, OnDes
|
||||
}
|
||||
|
||||
setDisabledState(isDisabled: boolean): void {
|
||||
this.el.nativeElement.disabled = isDisabled;
|
||||
this.elementRef.nativeElement.disabled = isDisabled;
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -87,7 +87,7 @@ export class ValueAccessor implements ControlValueAccessor, AfterViewInit, OnDes
|
||||
|
||||
// Listen for changes in validity, disabled, or pending states
|
||||
if (ngControl.statusChanges) {
|
||||
this.statusChanges = ngControl.statusChanges.subscribe(() => setIonicClasses(this.el));
|
||||
this.statusChanges = ngControl.statusChanges.subscribe(() => setIonicClasses(this.elementRef));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,7 +102,7 @@ export class ValueAccessor implements ControlValueAccessor, AfterViewInit, OnDes
|
||||
const oldFn = formControl[method].bind(formControl);
|
||||
formControl[method] = (...params: any[]) => {
|
||||
oldFn(...params);
|
||||
setIonicClasses(this.el);
|
||||
setIonicClasses(this.elementRef);
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -129,7 +129,7 @@ export const setIonicClasses = (element: ElementRef): void => {
|
||||
|
||||
const getClasses = (element: HTMLElement) => {
|
||||
const classList = element.classList;
|
||||
const classes = [];
|
||||
const classes: string[] = [];
|
||||
for (let i = 0; i < classList.length; i++) {
|
||||
const item = classList.item(i);
|
||||
if (item !== null && startsWith(item, 'ng-')) {
|
||||
@@ -0,0 +1,61 @@
|
||||
import { HostListener, Input, Optional, ElementRef, NgZone, ChangeDetectorRef, Directive } from '@angular/core';
|
||||
import type { Components } from '@ionic/core';
|
||||
import type { AnimationBuilder } from '@ionic/core/components';
|
||||
|
||||
import { Config } from '../../providers/config';
|
||||
import { NavController } from '../../providers/nav-controller';
|
||||
import { ProxyCmp } from '../../utils/proxy';
|
||||
|
||||
import { IonRouterOutlet } from './router-outlet';
|
||||
|
||||
const BACK_BUTTON_INPUTS = ['color', 'defaultHref', 'disabled', 'icon', 'mode', 'routerAnimation', 'text', 'type'];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export declare interface IonBackButton extends Components.IonBackButton {}
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: BACK_BUTTON_INPUTS,
|
||||
})
|
||||
@Directive({
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: BACK_BUTTON_INPUTS,
|
||||
})
|
||||
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
||||
export class IonBackButton {
|
||||
@Input()
|
||||
defaultHref: string | undefined;
|
||||
|
||||
@Input()
|
||||
routerAnimation: AnimationBuilder | undefined;
|
||||
|
||||
protected el: HTMLElement;
|
||||
|
||||
constructor(
|
||||
@Optional() private routerOutlet: IonRouterOutlet,
|
||||
private navCtrl: NavController,
|
||||
private config: Config,
|
||||
private r: ElementRef,
|
||||
protected z: NgZone,
|
||||
c: ChangeDetectorRef
|
||||
) {
|
||||
c.detach();
|
||||
this.el = this.r.nativeElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@HostListener('click', ['$event'])
|
||||
onClick(ev: Event): void {
|
||||
const defaultHref = this.defaultHref || this.config.get('backButtonDefaultHref');
|
||||
|
||||
if (this.routerOutlet?.canGoBack()) {
|
||||
this.navCtrl.setDirection('back', undefined, undefined, this.routerAnimation);
|
||||
this.routerOutlet.pop();
|
||||
ev.preventDefault();
|
||||
} else if (defaultHref != null) {
|
||||
this.navCtrl.navigateBack(defaultHref, { animation: this.routerAnimation });
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
52
packages/angular/common/src/directives/navigation/nav.ts
Normal file
52
packages/angular/common/src/directives/navigation/nav.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { ElementRef, Injector, EnvironmentInjector, NgZone, ChangeDetectorRef, Directive } from '@angular/core';
|
||||
import type { Components } from '@ionic/core';
|
||||
|
||||
import { AngularDelegate } from '../../providers/angular-delegate';
|
||||
import { ProxyCmp, proxyOutputs } from '../../utils/proxy';
|
||||
|
||||
const NAV_INPUTS = ['animated', 'animation', 'root', 'rootParams', 'swipeGesture'];
|
||||
|
||||
const NAV_METHODS = [
|
||||
'push',
|
||||
'insert',
|
||||
'insertPages',
|
||||
'pop',
|
||||
'popTo',
|
||||
'popToRoot',
|
||||
'removeIndex',
|
||||
'setRoot',
|
||||
'setPages',
|
||||
'getActive',
|
||||
'getByIndex',
|
||||
'canGoBack',
|
||||
'getPrevious',
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export declare interface IonNav extends Components.IonNav {}
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: NAV_INPUTS,
|
||||
methods: NAV_METHODS,
|
||||
})
|
||||
@Directive({
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: NAV_INPUTS,
|
||||
})
|
||||
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
||||
export class IonNav {
|
||||
protected el: HTMLElement;
|
||||
constructor(
|
||||
ref: ElementRef,
|
||||
environmentInjector: EnvironmentInjector,
|
||||
injector: Injector,
|
||||
angularDelegate: AngularDelegate,
|
||||
protected z: NgZone,
|
||||
c: ChangeDetectorRef
|
||||
) {
|
||||
c.detach();
|
||||
this.el = ref.nativeElement;
|
||||
ref.nativeElement.delegate = angularDelegate.create(environmentInjector, injector);
|
||||
proxyOutputs(this, this.el, ['ionNavDidChange', 'ionNavWillChange']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { LocationStrategy } from '@angular/common';
|
||||
import { ElementRef, OnChanges, OnInit, Directive, HostListener, Input, Optional } from '@angular/core';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import type { AnimationBuilder, RouterDirection } from '@ionic/core/components';
|
||||
|
||||
import { NavController } from '../../providers/nav-controller';
|
||||
|
||||
/**
|
||||
* Adds support for Ionic routing directions and animations to the base Angular router link directive.
|
||||
*
|
||||
* When the router link is clicked, the directive will assign the direction and
|
||||
* animation so that the routing integration will transition correctly.
|
||||
*/
|
||||
@Directive({
|
||||
selector: ':not(a):not(area)[routerLink]',
|
||||
})
|
||||
export class RouterLinkDelegateDirective implements OnInit, OnChanges {
|
||||
@Input()
|
||||
routerDirection: RouterDirection = 'forward';
|
||||
|
||||
@Input()
|
||||
routerAnimation?: AnimationBuilder;
|
||||
|
||||
constructor(
|
||||
private locationStrategy: LocationStrategy,
|
||||
private navCtrl: NavController,
|
||||
private elementRef: ElementRef,
|
||||
private router: Router,
|
||||
@Optional() private routerLink?: RouterLink
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.updateTargetUrlAndHref();
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.updateTargetUrlAndHref();
|
||||
}
|
||||
|
||||
private updateTargetUrlAndHref() {
|
||||
if (this.routerLink?.urlTree) {
|
||||
const href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.routerLink.urlTree));
|
||||
this.elementRef.nativeElement.href = href;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@HostListener('click', ['$event'])
|
||||
onClick(ev: UIEvent): void {
|
||||
this.navCtrl.setDirection(this.routerDirection, undefined, undefined, this.routerAnimation);
|
||||
|
||||
/**
|
||||
* This prevents the browser from
|
||||
* performing a page reload when pressing
|
||||
* an Ionic component with routerLink.
|
||||
* The page reload interferes with routing
|
||||
* and causes ion-back-button to disappear
|
||||
* since the local history is wiped on reload.
|
||||
*/
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
@Directive({
|
||||
selector: 'a[routerLink],area[routerLink]',
|
||||
})
|
||||
export class RouterLinkWithHrefDelegateDirective implements OnInit, OnChanges {
|
||||
@Input()
|
||||
routerDirection: RouterDirection = 'forward';
|
||||
|
||||
@Input()
|
||||
routerAnimation?: AnimationBuilder;
|
||||
|
||||
constructor(
|
||||
private locationStrategy: LocationStrategy,
|
||||
private navCtrl: NavController,
|
||||
private elementRef: ElementRef,
|
||||
private router: Router,
|
||||
@Optional() private routerLink?: RouterLink
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.updateTargetUrlAndHref();
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.updateTargetUrlAndHref();
|
||||
}
|
||||
|
||||
private updateTargetUrlAndHref() {
|
||||
if (this.routerLink?.urlTree) {
|
||||
const href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.routerLink.urlTree));
|
||||
this.elementRef.nativeElement.href = href;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@HostListener('click')
|
||||
onClick(): void {
|
||||
this.navCtrl.setDirection(this.routerDirection, undefined, undefined, this.routerAnimation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,540 @@
|
||||
import { Location } from '@angular/common';
|
||||
import {
|
||||
ComponentRef,
|
||||
ElementRef,
|
||||
Injector,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewContainerRef,
|
||||
inject,
|
||||
Attribute,
|
||||
Directive,
|
||||
EventEmitter,
|
||||
Optional,
|
||||
Output,
|
||||
SkipSelf,
|
||||
EnvironmentInjector,
|
||||
Input,
|
||||
InjectionToken,
|
||||
Injectable,
|
||||
reflectComponentType,
|
||||
} from '@angular/core';
|
||||
import type { Provider } from '@angular/core';
|
||||
import { OutletContext, Router, ActivatedRoute, ChildrenOutletContexts, PRIMARY_OUTLET, Data } from '@angular/router';
|
||||
import { componentOnReady } from '@ionic/core/components';
|
||||
import type { AnimationBuilder } from '@ionic/core/components';
|
||||
import { Observable, BehaviorSubject, Subscription, combineLatest, of } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { Config } from '../../providers/config';
|
||||
import { NavController } from '../../providers/nav-controller';
|
||||
|
||||
import { StackController } from './stack-controller';
|
||||
import { RouteView, StackDidChangeEvent, StackWillChangeEvent, getUrl, isTabSwitch } from './stack-utils';
|
||||
|
||||
// TODO(FW-2827): types
|
||||
|
||||
@Directive({
|
||||
selector: 'ion-router-outlet',
|
||||
exportAs: 'outlet',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['animated', 'animation', 'mode', 'swipeGesture'],
|
||||
})
|
||||
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
||||
export class IonRouterOutlet implements OnDestroy, OnInit {
|
||||
nativeEl: HTMLIonRouterOutletElement;
|
||||
activatedView: RouteView | null = null;
|
||||
tabsPrefix: string | undefined;
|
||||
|
||||
private _swipeGesture?: boolean;
|
||||
private stackCtrl: StackController;
|
||||
|
||||
// Maintain map of activated route proxies for each component instance
|
||||
private proxyMap = new WeakMap<any, ActivatedRoute>();
|
||||
// Keep the latest activated route in a subject for the proxy routes to switch map to
|
||||
private currentActivatedRoute$ = new BehaviorSubject<{ component: any; activatedRoute: ActivatedRoute } | null>(null);
|
||||
|
||||
private activated: ComponentRef<any> | null = null;
|
||||
/** @internal */
|
||||
get activatedComponentRef(): ComponentRef<any> | null {
|
||||
return this.activated;
|
||||
}
|
||||
private _activatedRoute: ActivatedRoute | null = null;
|
||||
|
||||
/**
|
||||
* The name of the outlet
|
||||
*/
|
||||
@Input() name = PRIMARY_OUTLET;
|
||||
|
||||
/** @internal */
|
||||
@Output() stackWillChange = new EventEmitter<StackWillChangeEvent>();
|
||||
/** @internal */
|
||||
@Output() stackDidChange = new EventEmitter<StackDidChangeEvent>();
|
||||
|
||||
// eslint-disable-next-line @angular-eslint/no-output-rename
|
||||
@Output('activate') activateEvents = new EventEmitter<any>();
|
||||
// eslint-disable-next-line @angular-eslint/no-output-rename
|
||||
@Output('deactivate') deactivateEvents = new EventEmitter<any>();
|
||||
|
||||
private parentContexts = inject(ChildrenOutletContexts);
|
||||
private location = inject(ViewContainerRef);
|
||||
private environmentInjector = inject(EnvironmentInjector);
|
||||
private inputBinder = inject(INPUT_BINDER, { optional: true });
|
||||
/** @nodoc */
|
||||
readonly supportsBindingToComponentInputs = true;
|
||||
|
||||
// Ionic providers
|
||||
private config = inject(Config);
|
||||
private navCtrl = inject(NavController);
|
||||
|
||||
set animation(animation: AnimationBuilder) {
|
||||
this.nativeEl.animation = animation;
|
||||
}
|
||||
|
||||
set animated(animated: boolean) {
|
||||
this.nativeEl.animated = animated;
|
||||
}
|
||||
|
||||
set swipeGesture(swipe: boolean) {
|
||||
this._swipeGesture = swipe;
|
||||
|
||||
this.nativeEl.swipeHandler = swipe
|
||||
? {
|
||||
canStart: () => this.stackCtrl.canGoBack(1) && !this.stackCtrl.hasRunningTask(),
|
||||
onStart: () => this.stackCtrl.startBackTransition(),
|
||||
onEnd: (shouldContinue) => this.stackCtrl.endBackTransition(shouldContinue),
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Attribute('name') name: string,
|
||||
@Optional() @Attribute('tabs') tabs: string,
|
||||
commonLocation: Location,
|
||||
elementRef: ElementRef,
|
||||
router: Router,
|
||||
zone: NgZone,
|
||||
activatedRoute: ActivatedRoute,
|
||||
@SkipSelf() @Optional() readonly parentOutlet?: IonRouterOutlet
|
||||
) {
|
||||
this.nativeEl = elementRef.nativeElement;
|
||||
this.name = name || PRIMARY_OUTLET;
|
||||
this.tabsPrefix = tabs === 'true' ? getUrl(router, activatedRoute) : undefined;
|
||||
this.stackCtrl = new StackController(this.tabsPrefix, this.nativeEl, router, this.navCtrl, zone, commonLocation);
|
||||
this.parentContexts.onChildOutletCreated(this.name, this as any);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.stackCtrl.destroy();
|
||||
this.inputBinder?.unsubscribeFromRouteData(this);
|
||||
}
|
||||
|
||||
getContext(): OutletContext | null {
|
||||
return this.parentContexts.getContext(this.name);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initializeOutletWithName();
|
||||
}
|
||||
|
||||
// Note: Ionic deviates from the Angular Router implementation here
|
||||
private initializeOutletWithName() {
|
||||
if (!this.activated) {
|
||||
// If the outlet was not instantiated at the time the route got activated we need to populate
|
||||
// the outlet when it is initialized (ie inside a NgIf)
|
||||
const context = this.getContext();
|
||||
if (context?.route) {
|
||||
this.activateWith(context.route, context.injector);
|
||||
}
|
||||
}
|
||||
|
||||
new Promise((resolve) => componentOnReady(this.nativeEl, resolve)).then(() => {
|
||||
if (this._swipeGesture === undefined) {
|
||||
this.swipeGesture = this.config.getBoolean('swipeBackEnabled', (this.nativeEl as any).mode === 'ios');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get isActivated(): boolean {
|
||||
return !!this.activated;
|
||||
}
|
||||
|
||||
get component(): Record<string, unknown> {
|
||||
if (!this.activated) {
|
||||
throw new Error('Outlet is not activated');
|
||||
}
|
||||
return this.activated.instance;
|
||||
}
|
||||
|
||||
get activatedRoute(): ActivatedRoute {
|
||||
if (!this.activated) {
|
||||
throw new Error('Outlet is not activated');
|
||||
}
|
||||
return this._activatedRoute as ActivatedRoute;
|
||||
}
|
||||
|
||||
get activatedRouteData(): Data {
|
||||
if (this._activatedRoute) {
|
||||
return this._activatedRoute.snapshot.data;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the `RouteReuseStrategy` instructs to detach the subtree
|
||||
*/
|
||||
detach(): ComponentRef<any> {
|
||||
throw new Error('incompatible reuse strategy');
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the `RouteReuseStrategy` instructs to re-attach a previously detached subtree
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
attach(_ref: ComponentRef<any>, _activatedRoute: ActivatedRoute): void {
|
||||
throw new Error('incompatible reuse strategy');
|
||||
}
|
||||
|
||||
deactivate(): void {
|
||||
if (this.activated) {
|
||||
if (this.activatedView) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const context = this.getContext()!;
|
||||
this.activatedView.savedData = new Map(context.children['contexts']);
|
||||
|
||||
/**
|
||||
* Angular v11.2.10 introduced a change
|
||||
* where this route context is cleared out when
|
||||
* a router-outlet is deactivated, However,
|
||||
* we need this route information in order to
|
||||
* return a user back to the correct tab when
|
||||
* leaving and then going back to the tab context.
|
||||
*/
|
||||
const primaryOutlet = this.activatedView.savedData.get('primary');
|
||||
if (primaryOutlet && context.route) {
|
||||
primaryOutlet.route = { ...context.route };
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure we are saving the NavigationExtras
|
||||
* data otherwise it will be lost
|
||||
*/
|
||||
this.activatedView.savedExtras = {};
|
||||
if (context.route) {
|
||||
const contextSnapshot = context.route.snapshot;
|
||||
|
||||
this.activatedView.savedExtras.queryParams = contextSnapshot.queryParams;
|
||||
(this.activatedView.savedExtras.fragment as string | null) = contextSnapshot.fragment;
|
||||
}
|
||||
}
|
||||
const c = this.component;
|
||||
this.activatedView = null;
|
||||
this.activated = null;
|
||||
this._activatedRoute = null;
|
||||
this.deactivateEvents.emit(c);
|
||||
}
|
||||
}
|
||||
|
||||
activateWith(activatedRoute: ActivatedRoute, environmentInjector: EnvironmentInjector | null): void {
|
||||
if (this.isActivated) {
|
||||
throw new Error('Cannot activate an already activated outlet');
|
||||
}
|
||||
this._activatedRoute = activatedRoute;
|
||||
|
||||
let cmpRef: any;
|
||||
let enteringView = this.stackCtrl.getExistingView(activatedRoute);
|
||||
if (enteringView) {
|
||||
cmpRef = this.activated = enteringView.ref;
|
||||
const saved = enteringView.savedData;
|
||||
if (saved) {
|
||||
// self-restore
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const context = this.getContext()!;
|
||||
context.children['contexts'] = saved;
|
||||
}
|
||||
// Updated activated route proxy for this component
|
||||
this.updateActivatedRouteProxy(cmpRef.instance, activatedRoute);
|
||||
} else {
|
||||
const snapshot = (activatedRoute as any)._futureSnapshot;
|
||||
|
||||
/**
|
||||
* Angular 14 introduces a new `loadComponent` property to the route config.
|
||||
* This function will assign a `component` property to the route snapshot.
|
||||
* We check for the presence of this property to determine if the route is
|
||||
* using standalone components.
|
||||
*/
|
||||
const childContexts = this.parentContexts.getOrCreateContext(this.name).children;
|
||||
|
||||
// We create an activated route proxy object that will maintain future updates for this component
|
||||
// over its lifecycle in the stack.
|
||||
const component$ = new BehaviorSubject<any>(null);
|
||||
const activatedRouteProxy = this.createActivatedRouteProxy(component$, activatedRoute);
|
||||
|
||||
const injector = new OutletInjector(activatedRouteProxy, childContexts, this.location.injector);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const component = snapshot.routeConfig!.component ?? snapshot.component;
|
||||
|
||||
cmpRef = this.activated = this.location.createComponent(component, {
|
||||
index: this.location.length,
|
||||
injector,
|
||||
environmentInjector: environmentInjector ?? this.environmentInjector,
|
||||
});
|
||||
|
||||
// Once the component is created we can push it to our local subject supplied to the proxy
|
||||
component$.next(cmpRef.instance);
|
||||
|
||||
// Calling `markForCheck` to make sure we will run the change detection when the
|
||||
// `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component.
|
||||
enteringView = this.stackCtrl.createView(this.activated, activatedRoute);
|
||||
|
||||
// Store references to the proxy by component
|
||||
this.proxyMap.set(cmpRef.instance, activatedRouteProxy);
|
||||
this.currentActivatedRoute$.next({ component: cmpRef.instance, activatedRoute });
|
||||
}
|
||||
|
||||
this.inputBinder?.bindActivatedRouteToOutletComponent(this);
|
||||
|
||||
this.activatedView = enteringView;
|
||||
|
||||
/**
|
||||
* The top outlet is set prior to the entering view's transition completing,
|
||||
* so that when we have nested outlets (e.g. ion-tabs inside an ion-router-outlet),
|
||||
* the tabs outlet will be assigned as the top outlet when a view inside tabs is
|
||||
* activated.
|
||||
*
|
||||
* In this scenario, activeWith is called for both the tabs and the root router outlet.
|
||||
* To avoid a race condition, we assign the top outlet synchronously.
|
||||
*/
|
||||
this.navCtrl.setTopOutlet(this);
|
||||
|
||||
const leavingView = this.stackCtrl.getActiveView();
|
||||
|
||||
this.stackWillChange.emit({
|
||||
enteringView,
|
||||
tabSwitch: isTabSwitch(enteringView, leavingView),
|
||||
});
|
||||
|
||||
this.stackCtrl.setActive(enteringView).then((data) => {
|
||||
this.activateEvents.emit(cmpRef.instance);
|
||||
this.stackDidChange.emit(data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if there are pages in the stack to go back.
|
||||
*/
|
||||
canGoBack(deep = 1, stackId?: string): boolean {
|
||||
return this.stackCtrl.canGoBack(deep, stackId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves to `true` if it the outlet was able to sucessfully pop the last N pages.
|
||||
*/
|
||||
pop(deep = 1, stackId?: string): Promise<boolean> {
|
||||
return this.stackCtrl.pop(deep, stackId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of the active page of each stack.
|
||||
*/
|
||||
getLastUrl(stackId?: string): string | undefined {
|
||||
const active = this.stackCtrl.getLastUrl(stackId);
|
||||
return active ? active.url : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RouteView of the active page of each stack.
|
||||
* @internal
|
||||
*/
|
||||
getLastRouteView(stackId?: string): RouteView | undefined {
|
||||
return this.stackCtrl.getLastUrl(stackId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root view in the tab stack.
|
||||
* @internal
|
||||
*/
|
||||
getRootView(stackId?: string): RouteView | undefined {
|
||||
return this.stackCtrl.getRootUrl(stackId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the active stack ID. In the context of ion-tabs, it means the active tab.
|
||||
*/
|
||||
getActiveStackId(): string | undefined {
|
||||
return this.stackCtrl.getActiveStackId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Since the activated route can change over the life time of a component in an ion router outlet, we create
|
||||
* a proxy so that we can update the values over time as a user navigates back to components already in the stack.
|
||||
*/
|
||||
private createActivatedRouteProxy(component$: Observable<any>, activatedRoute: ActivatedRoute): ActivatedRoute {
|
||||
const proxy: any = new ActivatedRoute();
|
||||
|
||||
proxy._futureSnapshot = (activatedRoute as any)._futureSnapshot;
|
||||
proxy._routerState = (activatedRoute as any)._routerState;
|
||||
proxy.snapshot = activatedRoute.snapshot;
|
||||
proxy.outlet = activatedRoute.outlet;
|
||||
proxy.component = activatedRoute.component;
|
||||
|
||||
// Setup wrappers for the observables so consumers don't have to worry about switching to new observables as the state updates
|
||||
(proxy as any)._paramMap = this.proxyObservable(component$, 'paramMap');
|
||||
(proxy as any)._queryParamMap = this.proxyObservable(component$, 'queryParamMap');
|
||||
proxy.url = this.proxyObservable(component$, 'url');
|
||||
proxy.params = this.proxyObservable(component$, 'params');
|
||||
proxy.queryParams = this.proxyObservable(component$, 'queryParams');
|
||||
proxy.fragment = this.proxyObservable(component$, 'fragment');
|
||||
proxy.data = this.proxyObservable(component$, 'data');
|
||||
|
||||
return proxy as ActivatedRoute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapped observable that will switch to the latest activated route matched by the given component
|
||||
*/
|
||||
private proxyObservable(component$: Observable<any>, path: string): Observable<any> {
|
||||
return component$.pipe(
|
||||
// First wait until the component instance is pushed
|
||||
filter((component) => !!component),
|
||||
switchMap((component) =>
|
||||
this.currentActivatedRoute$.pipe(
|
||||
filter((current) => current !== null && current.component === component),
|
||||
switchMap((current) => current && (current.activatedRoute as any)[path]),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the activated route proxy for the given component to the new incoming router state
|
||||
*/
|
||||
private updateActivatedRouteProxy(component: any, activatedRoute: ActivatedRoute): void {
|
||||
const proxy = this.proxyMap.get(component);
|
||||
if (!proxy) {
|
||||
throw new Error(`Could not find activated route proxy for view`);
|
||||
}
|
||||
|
||||
(proxy as any)._futureSnapshot = (activatedRoute as any)._futureSnapshot;
|
||||
(proxy as any)._routerState = (activatedRoute as any)._routerState;
|
||||
proxy.snapshot = activatedRoute.snapshot;
|
||||
proxy.outlet = activatedRoute.outlet;
|
||||
proxy.component = activatedRoute.component;
|
||||
|
||||
this.currentActivatedRoute$.next({ component, activatedRoute });
|
||||
}
|
||||
}
|
||||
|
||||
class OutletInjector implements Injector {
|
||||
constructor(private route: ActivatedRoute, private childContexts: ChildrenOutletContexts, private parent: Injector) {}
|
||||
|
||||
get(token: any, notFoundValue?: any): any {
|
||||
if (token === ActivatedRoute) {
|
||||
return this.route;
|
||||
}
|
||||
|
||||
if (token === ChildrenOutletContexts) {
|
||||
return this.childContexts;
|
||||
}
|
||||
|
||||
return this.parent.get(token, notFoundValue);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: FW-4785 - Remove this once Angular 15 support is dropped
|
||||
const INPUT_BINDER = new InjectionToken<RoutedComponentInputBinder>('');
|
||||
|
||||
/**
|
||||
* Injectable used as a tree-shakable provider for opting in to binding router data to component
|
||||
* inputs.
|
||||
*
|
||||
* The RouterOutlet registers itself with this service when an `ActivatedRoute` is attached or
|
||||
* activated. When this happens, the service subscribes to the `ActivatedRoute` observables (params,
|
||||
* queryParams, data) and sets the inputs of the component using `ComponentRef.setInput`.
|
||||
* Importantly, when an input does not have an item in the route data with a matching key, this
|
||||
* input is set to `undefined`. If it were not done this way, the previous information would be
|
||||
* retained if the data got removed from the route (i.e. if a query parameter is removed).
|
||||
*
|
||||
* The `RouterOutlet` should unregister itself when destroyed via `unsubscribeFromRouteData` so that
|
||||
* the subscriptions are cleaned up.
|
||||
*/
|
||||
@Injectable()
|
||||
class RoutedComponentInputBinder {
|
||||
private outletDataSubscriptions = new Map<IonRouterOutlet, Subscription>();
|
||||
|
||||
bindActivatedRouteToOutletComponent(outlet: IonRouterOutlet): void {
|
||||
this.unsubscribeFromRouteData(outlet);
|
||||
this.subscribeToRouteData(outlet);
|
||||
}
|
||||
|
||||
unsubscribeFromRouteData(outlet: IonRouterOutlet): void {
|
||||
this.outletDataSubscriptions.get(outlet)?.unsubscribe();
|
||||
this.outletDataSubscriptions.delete(outlet);
|
||||
}
|
||||
|
||||
private subscribeToRouteData(outlet: IonRouterOutlet) {
|
||||
const { activatedRoute } = outlet;
|
||||
const dataSubscription = combineLatest([activatedRoute.queryParams, activatedRoute.params, activatedRoute.data])
|
||||
.pipe(
|
||||
switchMap(([queryParams, params, data], index) => {
|
||||
data = { ...queryParams, ...params, ...data };
|
||||
// Get the first result from the data subscription synchronously so it's available to
|
||||
// the component as soon as possible (and doesn't require a second change detection).
|
||||
if (index === 0) {
|
||||
return of(data);
|
||||
}
|
||||
// Promise.resolve is used to avoid synchronously writing the wrong data when
|
||||
// two of the Observables in the `combineLatest` stream emit one after
|
||||
// another.
|
||||
return Promise.resolve(data);
|
||||
})
|
||||
)
|
||||
.subscribe((data) => {
|
||||
// Outlet may have been deactivated or changed names to be associated with a different
|
||||
// route
|
||||
if (
|
||||
!outlet.isActivated ||
|
||||
!outlet.activatedComponentRef ||
|
||||
outlet.activatedRoute !== activatedRoute ||
|
||||
activatedRoute.component === null
|
||||
) {
|
||||
this.unsubscribeFromRouteData(outlet);
|
||||
return;
|
||||
}
|
||||
|
||||
const mirror = reflectComponentType(activatedRoute.component);
|
||||
if (!mirror) {
|
||||
this.unsubscribeFromRouteData(outlet);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const { templateName } of mirror.inputs) {
|
||||
outlet.activatedComponentRef.setInput(templateName, data[templateName]);
|
||||
}
|
||||
});
|
||||
|
||||
this.outletDataSubscriptions.set(outlet, dataSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
export const provideComponentInputBinding = (): Provider => {
|
||||
return {
|
||||
provide: INPUT_BINDER,
|
||||
useFactory: componentInputBindingFactory,
|
||||
deps: [Router],
|
||||
};
|
||||
};
|
||||
|
||||
function componentInputBindingFactory(router?: Router) {
|
||||
/**
|
||||
* We cast the router to any here, since the componentInputBindingEnabled
|
||||
* property is not available until Angular v16.
|
||||
*/
|
||||
if ((router as any)?.componentInputBindingEnabled) {
|
||||
return new RoutedComponentInputBinder();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Location } from '@angular/common';
|
||||
import { ComponentRef, NgZone } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { AnimationBuilder, RouterDirection } from '@ionic/core';
|
||||
import type { AnimationBuilder, RouterDirection } from '@ionic/core/components';
|
||||
|
||||
import { bindLifecycleEvents } from '../../providers/angular-delegate';
|
||||
import { NavController } from '../../providers/nav-controller';
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ComponentRef } from '@angular/core';
|
||||
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
|
||||
import { AnimationBuilder, NavDirection, RouterDirection } from '@ionic/core';
|
||||
import type { AnimationBuilder, NavDirection, RouterDirection } from '@ionic/core/components';
|
||||
|
||||
export const insertView = (views: RouteView[], view: RouteView, direction: RouterDirection): RouteView[] => {
|
||||
if (direction === 'root') {
|
||||
192
packages/angular/common/src/directives/navigation/tabs.ts
Normal file
192
packages/angular/common/src/directives/navigation/tabs.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import {
|
||||
AfterContentChecked,
|
||||
AfterContentInit,
|
||||
Directive,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Output,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
|
||||
import { NavController } from '../../providers/nav-controller';
|
||||
|
||||
import { StackDidChangeEvent, StackWillChangeEvent } from './stack-utils';
|
||||
|
||||
@Directive({
|
||||
selector: 'ion-tabs',
|
||||
})
|
||||
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
||||
export class IonTabs implements AfterContentInit, AfterContentChecked {
|
||||
/**
|
||||
* Note: These must be redeclared on each child class since it needs
|
||||
* access to generated components such as IonRouterOutlet and IonTabBar.
|
||||
*/
|
||||
outlet: any;
|
||||
tabBar: any;
|
||||
tabBars: any;
|
||||
|
||||
@ViewChild('tabsInner', { read: ElementRef, static: true }) tabsInner: ElementRef<HTMLDivElement>;
|
||||
|
||||
/**
|
||||
* Emitted before the tab view is changed.
|
||||
*/
|
||||
@Output() ionTabsWillChange = new EventEmitter<{ tab: string }>();
|
||||
/**
|
||||
* Emitted after the tab view is changed.
|
||||
*/
|
||||
@Output() ionTabsDidChange = new EventEmitter<{ tab: string }>();
|
||||
|
||||
private tabBarSlot = 'bottom';
|
||||
|
||||
constructor(private navCtrl: NavController) {}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this.detectSlotChanges();
|
||||
}
|
||||
|
||||
ngAfterContentChecked(): void {
|
||||
this.detectSlotChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
onStackWillChange({ enteringView, tabSwitch }: StackWillChangeEvent): void {
|
||||
const stackId = enteringView.stackId;
|
||||
if (tabSwitch && stackId !== undefined) {
|
||||
this.ionTabsWillChange.emit({ tab: stackId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
onStackDidChange({ enteringView, tabSwitch }: StackDidChangeEvent): void {
|
||||
const stackId = enteringView.stackId;
|
||||
if (tabSwitch && stackId !== undefined) {
|
||||
if (this.tabBar) {
|
||||
this.tabBar.selectedTab = stackId;
|
||||
}
|
||||
this.ionTabsDidChange.emit({ tab: stackId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When a tab button is clicked, there are several scenarios:
|
||||
* 1. If the selected tab is currently active (the tab button has been clicked
|
||||
* again), then it should go to the root view for that tab.
|
||||
*
|
||||
* a. Get the saved root view from the router outlet. If the saved root view
|
||||
* matches the tabRootUrl, set the route view to this view including the
|
||||
* navigation extras.
|
||||
* b. If the saved root view from the router outlet does
|
||||
* not match, navigate to the tabRootUrl. No navigation extras are
|
||||
* included.
|
||||
*
|
||||
* 2. If the current tab tab is not currently selected, get the last route
|
||||
* view from the router outlet.
|
||||
*
|
||||
* a. If the last route view exists, navigate to that view including any
|
||||
* navigation extras
|
||||
* b. If the last route view doesn't exist, then navigate
|
||||
* to the default tabRootUrl
|
||||
*/
|
||||
@HostListener('ionTabButtonClick', ['$event'])
|
||||
select(tabOrEvent: string | CustomEvent): Promise<boolean> | undefined {
|
||||
const isTabString = typeof tabOrEvent === 'string';
|
||||
const tab = isTabString ? tabOrEvent : (tabOrEvent as CustomEvent).detail.tab;
|
||||
const alreadySelected = this.outlet.getActiveStackId() === tab;
|
||||
const tabRootUrl = `${this.outlet.tabsPrefix}/${tab}`;
|
||||
|
||||
/**
|
||||
* If this is a nested tab, prevent the event
|
||||
* from bubbling otherwise the outer tabs
|
||||
* will respond to this event too, causing
|
||||
* the app to get directed to the wrong place.
|
||||
*/
|
||||
if (!isTabString) {
|
||||
(tabOrEvent as CustomEvent).stopPropagation();
|
||||
}
|
||||
|
||||
if (alreadySelected) {
|
||||
const activeStackId = this.outlet.getActiveStackId();
|
||||
const activeView = this.outlet.getLastRouteView(activeStackId);
|
||||
|
||||
// If on root tab, do not navigate to root tab again
|
||||
if (activeView?.url === tabRootUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rootView = this.outlet.getRootView(tab);
|
||||
const navigationExtras = rootView && tabRootUrl === rootView.url && rootView.savedExtras;
|
||||
return this.navCtrl.navigateRoot(tabRootUrl, {
|
||||
...navigationExtras,
|
||||
animated: true,
|
||||
animationDirection: 'back',
|
||||
});
|
||||
} else {
|
||||
const lastRoute = this.outlet.getLastRouteView(tab);
|
||||
/**
|
||||
* If there is a lastRoute, goto that, otherwise goto the fallback url of the
|
||||
* selected tab
|
||||
*/
|
||||
const url = lastRoute?.url || tabRootUrl;
|
||||
const navigationExtras = lastRoute?.savedExtras;
|
||||
|
||||
return this.navCtrl.navigateRoot(url, {
|
||||
...navigationExtras,
|
||||
animated: true,
|
||||
animationDirection: 'back',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
42
packages/angular/common/src/index.ts
Normal file
42
packages/angular/common/src/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
export { ActionSheetController } from './providers/action-sheet-controller';
|
||||
export { AlertController } from './providers/alert-controller';
|
||||
export { LoadingController } from './providers/loading-controller';
|
||||
export { MenuController } from './providers/menu-controller';
|
||||
export { ModalController } from './providers/modal-controller';
|
||||
export { PickerController } from './providers/picker-controller';
|
||||
export { PopoverController } from './providers/popover-controller';
|
||||
export { ToastController } from './providers/toast-controller';
|
||||
|
||||
export { AnimationController } from './providers/animation-controller';
|
||||
export { GestureController } from './providers/gesture-controller';
|
||||
export { DomController } from './providers/dom-controller';
|
||||
export { NavController } from './providers/nav-controller';
|
||||
|
||||
export { Config, ConfigToken } from './providers/config';
|
||||
export { Platform } from './providers/platform';
|
||||
|
||||
export { bindLifecycleEvents, AngularDelegate } from './providers/angular-delegate';
|
||||
|
||||
export type { IonicWindow } from './types/interfaces';
|
||||
|
||||
export { NavParams } from './directives/navigation/nav-params';
|
||||
|
||||
export { IonPopover } from './overlays/popover';
|
||||
export { IonModal } from './overlays/modal';
|
||||
|
||||
export { IonRouterOutlet, provideComponentInputBinding } from './directives/navigation/router-outlet';
|
||||
|
||||
export { IonBackButton } from './directives/navigation/back-button';
|
||||
export {
|
||||
RouterLinkDelegateDirective,
|
||||
RouterLinkWithHrefDelegateDirective,
|
||||
} from './directives/navigation/router-link-delegate';
|
||||
export { IonNav } from './directives/navigation/nav';
|
||||
export { IonTabs } from './directives/navigation/tabs';
|
||||
export * from './directives/control-value-accessors';
|
||||
|
||||
export { ProxyCmp } from './utils/proxy';
|
||||
|
||||
export { IonicRouteStrategy } from './utils/routing';
|
||||
|
||||
export { raf } from './utils/util';
|
||||
133
packages/angular/common/src/overlays/modal.ts
Normal file
133
packages/angular/common/src/overlays/modal.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
ContentChild,
|
||||
Directive,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
NgZone,
|
||||
TemplateRef,
|
||||
} from '@angular/core';
|
||||
import type { Components, ModalBreakpointChangeEventDetail } from '@ionic/core/components';
|
||||
|
||||
import { ProxyCmp, proxyOutputs } from '../utils/proxy';
|
||||
|
||||
export declare interface IonModal extends Components.IonModal {
|
||||
/**
|
||||
* Emitted after the modal has presented.
|
||||
**/
|
||||
ionModalDidPresent: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted before the modal has presented.
|
||||
*/
|
||||
ionModalWillPresent: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted before the modal has dismissed.
|
||||
*/
|
||||
ionModalWillDismiss: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted after the modal has dismissed.
|
||||
*/
|
||||
ionModalDidDismiss: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted after the modal breakpoint has changed.
|
||||
*/
|
||||
ionBreakpointDidChange: EventEmitter<CustomEvent<ModalBreakpointChangeEventDetail>>;
|
||||
/**
|
||||
* Emitted after the modal has presented. Shorthand for ionModalDidPresent.
|
||||
*/
|
||||
didPresent: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted before the modal has presented. Shorthand for ionModalWillPresent.
|
||||
*/
|
||||
willPresent: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted before the modal has dismissed. Shorthand for ionModalWillDismiss.
|
||||
*/
|
||||
willDismiss: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted after the modal has dismissed. Shorthand for ionModalDidDismiss.
|
||||
*/
|
||||
didDismiss: EventEmitter<CustomEvent>;
|
||||
}
|
||||
|
||||
const MODAL_INPUTS = [
|
||||
'animated',
|
||||
'keepContentsMounted',
|
||||
'backdropBreakpoint',
|
||||
'backdropDismiss',
|
||||
'breakpoints',
|
||||
'canDismiss',
|
||||
'cssClass',
|
||||
'enterAnimation',
|
||||
'event',
|
||||
'handle',
|
||||
'handleBehavior',
|
||||
'initialBreakpoint',
|
||||
'isOpen',
|
||||
'keyboardClose',
|
||||
'leaveAnimation',
|
||||
'mode',
|
||||
'presentingElement',
|
||||
'showBackdrop',
|
||||
'translucent',
|
||||
'trigger',
|
||||
];
|
||||
|
||||
const MODAL_METHODS = [
|
||||
'present',
|
||||
'dismiss',
|
||||
'onDidDismiss',
|
||||
'onWillDismiss',
|
||||
'setCurrentBreakpoint',
|
||||
'getCurrentBreakpoint',
|
||||
];
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: MODAL_INPUTS,
|
||||
methods: MODAL_METHODS,
|
||||
})
|
||||
/**
|
||||
* @Component extends from @Directive
|
||||
* so by defining the inputs here we
|
||||
* do not need to re-define them for the
|
||||
* lazy loaded popover.
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'ion-modal',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: MODAL_INPUTS,
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
||||
export class IonModal {
|
||||
// TODO(FW-2827): type
|
||||
@ContentChild(TemplateRef, { static: false }) template: TemplateRef<any>;
|
||||
|
||||
isCmpOpen = false;
|
||||
|
||||
protected el: HTMLElement;
|
||||
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
||||
this.el = r.nativeElement;
|
||||
|
||||
this.el.addEventListener('ionMount', () => {
|
||||
this.isCmpOpen = true;
|
||||
c.detectChanges();
|
||||
});
|
||||
this.el.addEventListener('didDismiss', () => {
|
||||
this.isCmpOpen = false;
|
||||
c.detectChanges();
|
||||
});
|
||||
proxyOutputs(this, this.el, [
|
||||
'ionModalDidPresent',
|
||||
'ionModalWillPresent',
|
||||
'ionModalWillDismiss',
|
||||
'ionModalDidDismiss',
|
||||
'ionBreakpointDidChange',
|
||||
'didPresent',
|
||||
'willPresent',
|
||||
'willDismiss',
|
||||
'didDismiss',
|
||||
]);
|
||||
}
|
||||
}
|
||||
121
packages/angular/common/src/overlays/popover.ts
Normal file
121
packages/angular/common/src/overlays/popover.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
ContentChild,
|
||||
Directive,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
NgZone,
|
||||
TemplateRef,
|
||||
} from '@angular/core';
|
||||
import type { Components } from '@ionic/core/components';
|
||||
|
||||
import { ProxyCmp, proxyOutputs } from '../utils/proxy';
|
||||
|
||||
export declare interface IonPopover extends Components.IonPopover {
|
||||
/**
|
||||
* Emitted after the popover has presented.
|
||||
*/
|
||||
ionPopoverDidPresent: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted before the popover has presented.
|
||||
*/
|
||||
ionPopoverWillPresent: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted after the popover has dismissed.
|
||||
*/
|
||||
ionPopoverWillDismiss: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted after the popover has dismissed.
|
||||
*/
|
||||
ionPopoverDidDismiss: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted after the popover has presented. Shorthand for ionPopoverDidPresent.
|
||||
*/
|
||||
didPresent: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted before the popover has presented. Shorthand for ionPopoverWillPresent.
|
||||
*/
|
||||
willPresent: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted after the popover has presented. Shorthand for ionPopoverWillDismiss.
|
||||
*/
|
||||
willDismiss: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted after the popover has dismissed. Shorthand for ionPopoverDidDismiss.
|
||||
*/
|
||||
didDismiss: EventEmitter<CustomEvent>;
|
||||
}
|
||||
|
||||
const POPOVER_INPUTS = [
|
||||
'alignment',
|
||||
'animated',
|
||||
'arrow',
|
||||
'keepContentsMounted',
|
||||
'backdropDismiss',
|
||||
'cssClass',
|
||||
'dismissOnSelect',
|
||||
'enterAnimation',
|
||||
'event',
|
||||
'isOpen',
|
||||
'keyboardClose',
|
||||
'leaveAnimation',
|
||||
'mode',
|
||||
'showBackdrop',
|
||||
'translucent',
|
||||
'trigger',
|
||||
'triggerAction',
|
||||
'reference',
|
||||
'size',
|
||||
'side',
|
||||
];
|
||||
|
||||
const POPOVER_METHODS = ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss'];
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: POPOVER_INPUTS,
|
||||
methods: POPOVER_METHODS,
|
||||
})
|
||||
/**
|
||||
* @Component extends from @Directive
|
||||
* so by defining the inputs here we
|
||||
* do not need to re-define them for the
|
||||
* lazy loaded popover.
|
||||
*/
|
||||
@Directive({
|
||||
selector: 'ion-popover',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: POPOVER_INPUTS,
|
||||
})
|
||||
|
||||
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
||||
export class IonPopover {
|
||||
// TODO(FW-2827): type
|
||||
@ContentChild(TemplateRef, { static: false }) template: TemplateRef<any>;
|
||||
|
||||
isCmpOpen = false;
|
||||
|
||||
protected el: HTMLElement;
|
||||
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
||||
this.el = r.nativeElement;
|
||||
|
||||
this.el.addEventListener('ionMount', () => {
|
||||
this.isCmpOpen = true;
|
||||
c.detectChanges();
|
||||
});
|
||||
this.el.addEventListener('didDismiss', () => {
|
||||
this.isCmpOpen = false;
|
||||
c.detectChanges();
|
||||
});
|
||||
proxyOutputs(this, this.el, [
|
||||
'ionPopoverDidPresent',
|
||||
'ionPopoverWillPresent',
|
||||
'ionPopoverWillDismiss',
|
||||
'ionPopoverDidDismiss',
|
||||
'didPresent',
|
||||
'willPresent',
|
||||
'willDismiss',
|
||||
'didDismiss',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActionSheetOptions, actionSheetController } from '@ionic/core';
|
||||
import type { ActionSheetOptions } from '@ionic/core/components';
|
||||
import { actionSheetController } from '@ionic/core/components';
|
||||
|
||||
import { OverlayBaseController } from '../util/overlay';
|
||||
import { OverlayBaseController } from '../utils/overlay';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AlertOptions, alertController } from '@ionic/core';
|
||||
import type { AlertOptions } from '@ionic/core/components';
|
||||
import { alertController } from '@ionic/core/components';
|
||||
|
||||
import { OverlayBaseController } from '../util/overlay';
|
||||
import { OverlayBaseController } from '../utils/overlay';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
LIFECYCLE_WILL_ENTER,
|
||||
LIFECYCLE_WILL_LEAVE,
|
||||
LIFECYCLE_WILL_UNLOAD,
|
||||
} from '@ionic/core';
|
||||
} from '@ionic/core/components';
|
||||
|
||||
import { NavParams } from '../directives/navigation/nav-params';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Animation, createAnimation, getTimeGivenProgression } from '@ionic/core';
|
||||
import type { Animation } from '@ionic/core/components';
|
||||
import { createAnimation, getTimeGivenProgression } from '@ionic/core/components';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable, InjectionToken } from '@angular/core';
|
||||
import { Config as CoreConfig, IonicConfig } from '@ionic/core';
|
||||
import type { Config as CoreConfig, IonicConfig } from '@ionic/core/components';
|
||||
|
||||
import { IonicWindow } from '../types/interfaces';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NgZone, Injectable } from '@angular/core';
|
||||
import { Gesture, GestureConfig, createGesture } from '@ionic/core';
|
||||
import type { Gesture, GestureConfig } from '@ionic/core/components';
|
||||
import { createGesture } from '@ionic/core/components';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { LoadingOptions, loadingController } from '@ionic/core';
|
||||
import type { LoadingOptions } from '@ionic/core/components';
|
||||
import { loadingController } from '@ionic/core/components';
|
||||
|
||||
import { OverlayBaseController } from '../util/overlay';
|
||||
import { OverlayBaseController } from '../utils/overlay';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { menuController } from '@ionic/core';
|
||||
import { menuController } from '@ionic/core/components';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Injector, Injectable, EnvironmentInjector, inject } from '@angular/core';
|
||||
import { ModalOptions, modalController } from '@ionic/core';
|
||||
import type { ModalOptions } from '@ionic/core/components';
|
||||
import { modalController } from '@ionic/core/components';
|
||||
|
||||
import { OverlayBaseController } from '../util/overlay';
|
||||
import { OverlayBaseController } from '../utils/overlay';
|
||||
|
||||
import { AngularDelegate } from './angular-delegate';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Location } from '@angular/common';
|
||||
import { Injectable, Optional } from '@angular/core';
|
||||
import { NavigationExtras, Router, UrlSerializer, UrlTree, NavigationStart } from '@angular/router';
|
||||
import { AnimationBuilder, NavDirection, RouterDirection } from '@ionic/core';
|
||||
import type { AnimationBuilder, NavDirection, RouterDirection } from '@ionic/core/components';
|
||||
|
||||
import { IonRouterOutlet } from '../directives/navigation/ion-router-outlet';
|
||||
import { IonRouterOutlet } from '../directives/navigation/router-outlet';
|
||||
|
||||
import { Platform } from './platform';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { PickerOptions, pickerController } from '@ionic/core';
|
||||
import type { PickerOptions } from '@ionic/core/components';
|
||||
import { pickerController } from '@ionic/core/components';
|
||||
|
||||
import { OverlayBaseController } from '../util/overlay';
|
||||
import { OverlayBaseController } from '../utils/overlay';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -1,7 +1,10 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { Inject, Injectable, NgZone } from '@angular/core';
|
||||
import { BackButtonEventDetail, KeyboardEventDetail, Platforms, getPlatforms, isPlatform } from '@ionic/core';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { NgZone, Inject, Injectable } from '@angular/core';
|
||||
import { getPlatforms, isPlatform } from '@ionic/core/components';
|
||||
import type { BackButtonEventDetail, KeyboardEventDetail, Platforms } from '@ionic/core/components';
|
||||
import { Subscription, Subject } from 'rxjs';
|
||||
|
||||
// TODO(FW-2827): types
|
||||
|
||||
export interface BackButtonEmitter extends Subject<BackButtonEventDetail> {
|
||||
subscribeWithPriority(
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Injector, Injectable, inject, EnvironmentInjector } from '@angular/core';
|
||||
import { PopoverOptions, popoverController } from '@ionic/core';
|
||||
import type { PopoverOptions } from '@ionic/core/components';
|
||||
import { popoverController } from '@ionic/core/components';
|
||||
|
||||
import { OverlayBaseController } from '../util/overlay';
|
||||
import { OverlayBaseController } from '../utils/overlay';
|
||||
|
||||
import { AngularDelegate } from './angular-delegate';
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ToastOptions, toastController } from '@ionic/core';
|
||||
import type { ToastOptions } from '@ionic/core/components';
|
||||
import { toastController } from '@ionic/core/components';
|
||||
|
||||
import { OverlayBaseController } from '../util/overlay';
|
||||
import { OverlayBaseController } from '../utils/overlay';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -7,8 +7,3 @@ export interface IonicWindow extends Window {
|
||||
Ionic: IonicGlobal;
|
||||
__zone_symbol__requestAnimationFrame?: (ts: FrameRequestCallback) => number;
|
||||
}
|
||||
|
||||
export interface HTMLStencilElement extends HTMLElement {
|
||||
componentOnReady?(): Promise<this>;
|
||||
forceUpdate?(): void;
|
||||
}
|
||||
53
packages/angular/common/src/utils/proxy.ts
Normal file
53
packages/angular/common/src/utils/proxy.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
// TODO: Is there a way we can grab this from angular-component-lib instead?
|
||||
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import { fromEvent } from 'rxjs';
|
||||
|
||||
export const proxyInputs = (Cmp: any, inputs: string[]) => {
|
||||
const Prototype = Cmp.prototype;
|
||||
inputs.forEach((item) => {
|
||||
Object.defineProperty(Prototype, item, {
|
||||
get() {
|
||||
return this.el[item];
|
||||
},
|
||||
set(val: any) {
|
||||
this.z.runOutsideAngular(() => (this.el[item] = val));
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const proxyMethods = (Cmp: any, methods: string[]) => {
|
||||
const Prototype = Cmp.prototype;
|
||||
methods.forEach((methodName) => {
|
||||
Prototype[methodName] = function () {
|
||||
const args = arguments;
|
||||
return this.z.runOutsideAngular(() => this.el[methodName].apply(this.el, args));
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const proxyOutputs = (instance: any, el: any, events: string[]) => {
|
||||
events.forEach((eventName) => (instance[eventName] = fromEvent(el, eventName)));
|
||||
};
|
||||
|
||||
// tslint:disable-next-line: only-arrow-functions
|
||||
export function ProxyCmp(opts: { defineCustomElementFn?: () => void; inputs?: any; methods?: any }) {
|
||||
const decorator = function (cls: any) {
|
||||
const { defineCustomElementFn, inputs, methods } = opts;
|
||||
|
||||
if (defineCustomElementFn !== undefined) {
|
||||
defineCustomElementFn();
|
||||
}
|
||||
|
||||
if (inputs) {
|
||||
proxyInputs(cls, inputs);
|
||||
}
|
||||
if (methods) {
|
||||
proxyMethods(cls, methods);
|
||||
}
|
||||
return cls;
|
||||
};
|
||||
return decorator;
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
import { NgZone } from '@angular/core';
|
||||
import type { Config, IonicWindow } from '@ionic/angular/common';
|
||||
import { raf } from '@ionic/angular/common';
|
||||
import { setupConfig } from '@ionic/core';
|
||||
import { applyPolyfills, defineCustomElements } from '@ionic/core/loader';
|
||||
|
||||
import { Config } from './providers/config';
|
||||
import { IonicWindow } from './types/interfaces';
|
||||
import { raf } from './util/util';
|
||||
|
||||
// TODO(FW-2827): types
|
||||
|
||||
export const appInitialize = (config: Config, doc: Document, zone: NgZone) => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Directive, HostListener, ElementRef, Injector } from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
import { ValueAccessor, setIonicClasses } from './value-accessor';
|
||||
import { ValueAccessor, setIonicClasses } from '@ionic/angular/common';
|
||||
|
||||
@Directive({
|
||||
selector: 'ion-checkbox,ion-toggle',
|
||||
@@ -19,8 +18,8 @@ export class BooleanValueAccessorDirective extends ValueAccessor {
|
||||
}
|
||||
|
||||
writeValue(value: boolean): void {
|
||||
this.el.nativeElement.checked = this.lastValue = value;
|
||||
setIonicClasses(this.el);
|
||||
this.elementRef.nativeElement.checked = this.lastValue = value;
|
||||
setIonicClasses(this.elementRef);
|
||||
}
|
||||
|
||||
@HostListener('ionChange', ['$event.target'])
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Directive, HostListener, ElementRef, Injector } from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
import { ValueAccessor } from './value-accessor';
|
||||
import { ValueAccessor } from '@ionic/angular/common';
|
||||
|
||||
@Directive({
|
||||
selector: 'ion-input[type=number]',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ElementRef, Injector, Directive, HostListener } from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
import { ValueAccessor } from './value-accessor';
|
||||
import { ValueAccessor } from '@ionic/angular/common';
|
||||
|
||||
@Directive({
|
||||
/* tslint:disable-next-line:directive-selector */
|
||||
@@ -19,9 +18,12 @@ export class RadioValueAccessorDirective extends ValueAccessor {
|
||||
super(injector, el);
|
||||
}
|
||||
|
||||
// TODO(FW-2827): type (HTMLIonRadioElement and HTMLElement are both missing `checked`)
|
||||
@HostListener('ionSelect', ['$event.target'])
|
||||
_handleIonSelect(el: any): void {
|
||||
/**
|
||||
* The `el` type is any to access the `checked` state property
|
||||
* that is not exposed on the type interface.
|
||||
*/
|
||||
this.handleValueChange(el, el.checked);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ElementRef, Injector, Directive, HostListener } from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
import { ValueAccessor } from './value-accessor';
|
||||
import { ValueAccessor } from '@ionic/angular/common';
|
||||
|
||||
@Directive({
|
||||
/* tslint:disable-next-line:directive-selector */
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ElementRef, Injector, Directive, HostListener } from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
|
||||
import { ValueAccessor } from './value-accessor';
|
||||
import { ValueAccessor } from '@ionic/angular/common';
|
||||
|
||||
@Directive({
|
||||
selector: 'ion-input:not([type=number]),ion-textarea,ion-searchbar',
|
||||
|
||||
@@ -1,41 +1,23 @@
|
||||
import { Directive, HostListener, Input, Optional } from '@angular/core';
|
||||
import { AnimationBuilder } from '@ionic/core';
|
||||
|
||||
import { Config } from '../../providers/config';
|
||||
import { NavController } from '../../providers/nav-controller';
|
||||
import { Optional, ElementRef, NgZone, ChangeDetectorRef, Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { IonBackButton as IonBackButtonBase, NavController, Config } from '@ionic/angular/common';
|
||||
|
||||
import { IonRouterOutlet } from './ion-router-outlet';
|
||||
|
||||
@Directive({
|
||||
@Component({
|
||||
selector: 'ion-back-button',
|
||||
template: '<ng-content></ng-content>',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class IonBackButtonDelegateDirective {
|
||||
@Input()
|
||||
defaultHref: string | undefined | null;
|
||||
|
||||
@Input()
|
||||
routerAnimation?: AnimationBuilder;
|
||||
|
||||
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
||||
export class IonBackButton extends IonBackButtonBase {
|
||||
constructor(
|
||||
@Optional() private routerOutlet: IonRouterOutlet,
|
||||
private navCtrl: NavController,
|
||||
private config: Config
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@HostListener('click', ['$event'])
|
||||
onClick(ev: Event): void {
|
||||
const defaultHref = this.defaultHref || this.config.get('backButtonDefaultHref');
|
||||
|
||||
if (this.routerOutlet?.canGoBack()) {
|
||||
this.navCtrl.setDirection('back', undefined, undefined, this.routerAnimation);
|
||||
this.routerOutlet.pop();
|
||||
ev.preventDefault();
|
||||
} else if (defaultHref != null) {
|
||||
this.navCtrl.navigateBack(defaultHref, { animation: this.routerAnimation });
|
||||
ev.preventDefault();
|
||||
}
|
||||
@Optional() routerOutlet: IonRouterOutlet,
|
||||
navCtrl: NavController,
|
||||
config: Config,
|
||||
r: ElementRef,
|
||||
z: NgZone,
|
||||
c: ChangeDetectorRef
|
||||
) {
|
||||
super(routerOutlet, navCtrl, config, r, z, c);
|
||||
}
|
||||
}
|
||||
|
||||
29
packages/angular/src/directives/navigation/ion-nav.ts
Normal file
29
packages/angular/src/directives/navigation/ion-nav.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
ElementRef,
|
||||
Injector,
|
||||
EnvironmentInjector,
|
||||
NgZone,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
} from '@angular/core';
|
||||
import { IonNav as IonNavBase, AngularDelegate } from '@ionic/angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'ion-nav',
|
||||
template: '<ng-content></ng-content>',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
||||
export class IonNav extends IonNavBase {
|
||||
constructor(
|
||||
ref: ElementRef,
|
||||
environmentInjector: EnvironmentInjector,
|
||||
injector: Injector,
|
||||
angularDelegate: AngularDelegate,
|
||||
z: NgZone,
|
||||
c: ChangeDetectorRef
|
||||
) {
|
||||
super(ref, environmentInjector, injector, angularDelegate, z, c);
|
||||
}
|
||||
}
|
||||
@@ -1,520 +1,8 @@
|
||||
import { Location } from '@angular/common';
|
||||
import {
|
||||
ComponentRef,
|
||||
ElementRef,
|
||||
Injector,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ViewContainerRef,
|
||||
inject,
|
||||
Attribute,
|
||||
Directive,
|
||||
EventEmitter,
|
||||
Optional,
|
||||
Output,
|
||||
SkipSelf,
|
||||
EnvironmentInjector,
|
||||
Input,
|
||||
InjectionToken,
|
||||
Injectable,
|
||||
reflectComponentType,
|
||||
} from '@angular/core';
|
||||
import { OutletContext, Router, ActivatedRoute, ChildrenOutletContexts, PRIMARY_OUTLET, Data } from '@angular/router';
|
||||
import { componentOnReady } from '@ionic/core';
|
||||
import { Observable, BehaviorSubject, Subscription, combineLatest, of } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { AnimationBuilder } from '../../ionic-core';
|
||||
import { Config } from '../../providers/config';
|
||||
import { NavController } from '../../providers/nav-controller';
|
||||
|
||||
import { StackController } from './stack-controller';
|
||||
import { RouteView, StackDidChangeEvent, StackWillChangeEvent, getUrl, isTabSwitch } from './stack-utils';
|
||||
|
||||
// TODO(FW-2827): types
|
||||
import { Directive } from '@angular/core';
|
||||
import { IonRouterOutlet as IonRouterOutletBase } from '@ionic/angular/common';
|
||||
|
||||
@Directive({
|
||||
selector: 'ion-router-outlet',
|
||||
exportAs: 'outlet',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['animated', 'animation', 'mode', 'swipeGesture'],
|
||||
})
|
||||
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
||||
export class IonRouterOutlet implements OnDestroy, OnInit {
|
||||
nativeEl: HTMLIonRouterOutletElement;
|
||||
activatedView: RouteView | null = null;
|
||||
tabsPrefix: string | undefined;
|
||||
|
||||
private _swipeGesture?: boolean;
|
||||
private stackCtrl: StackController;
|
||||
|
||||
// Maintain map of activated route proxies for each component instance
|
||||
private proxyMap = new WeakMap<any, ActivatedRoute>();
|
||||
// Keep the latest activated route in a subject for the proxy routes to switch map to
|
||||
private currentActivatedRoute$ = new BehaviorSubject<{ component: any; activatedRoute: ActivatedRoute } | null>(null);
|
||||
|
||||
private activated: ComponentRef<any> | null = null;
|
||||
/** @internal */
|
||||
get activatedComponentRef(): ComponentRef<any> | null {
|
||||
return this.activated;
|
||||
}
|
||||
private _activatedRoute: ActivatedRoute | null = null;
|
||||
|
||||
/**
|
||||
* The name of the outlet
|
||||
*/
|
||||
@Input() name = PRIMARY_OUTLET;
|
||||
|
||||
/** @internal */
|
||||
@Output() stackWillChange = new EventEmitter<StackWillChangeEvent>();
|
||||
/** @internal */
|
||||
@Output() stackDidChange = new EventEmitter<StackDidChangeEvent>();
|
||||
|
||||
// eslint-disable-next-line @angular-eslint/no-output-rename
|
||||
@Output('activate') activateEvents = new EventEmitter<any>();
|
||||
// eslint-disable-next-line @angular-eslint/no-output-rename
|
||||
@Output('deactivate') deactivateEvents = new EventEmitter<any>();
|
||||
|
||||
private parentContexts = inject(ChildrenOutletContexts);
|
||||
private location = inject(ViewContainerRef);
|
||||
private environmentInjector = inject(EnvironmentInjector);
|
||||
private inputBinder = inject(INPUT_BINDER, { optional: true });
|
||||
/** @nodoc */
|
||||
readonly supportsBindingToComponentInputs = true;
|
||||
|
||||
// Ionic providers
|
||||
private config = inject(Config);
|
||||
private navCtrl = inject(NavController);
|
||||
|
||||
set animation(animation: AnimationBuilder) {
|
||||
this.nativeEl.animation = animation;
|
||||
}
|
||||
|
||||
set animated(animated: boolean) {
|
||||
this.nativeEl.animated = animated;
|
||||
}
|
||||
|
||||
set swipeGesture(swipe: boolean) {
|
||||
this._swipeGesture = swipe;
|
||||
|
||||
this.nativeEl.swipeHandler = swipe
|
||||
? {
|
||||
canStart: () => this.stackCtrl.canGoBack(1) && !this.stackCtrl.hasRunningTask(),
|
||||
onStart: () => this.stackCtrl.startBackTransition(),
|
||||
onEnd: (shouldContinue) => this.stackCtrl.endBackTransition(shouldContinue),
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
constructor(
|
||||
@Attribute('name') name: string,
|
||||
@Optional() @Attribute('tabs') tabs: string,
|
||||
commonLocation: Location,
|
||||
elementRef: ElementRef,
|
||||
router: Router,
|
||||
zone: NgZone,
|
||||
activatedRoute: ActivatedRoute,
|
||||
@SkipSelf() @Optional() readonly parentOutlet?: IonRouterOutlet
|
||||
) {
|
||||
this.nativeEl = elementRef.nativeElement;
|
||||
this.name = name || PRIMARY_OUTLET;
|
||||
this.tabsPrefix = tabs === 'true' ? getUrl(router, activatedRoute) : undefined;
|
||||
this.stackCtrl = new StackController(this.tabsPrefix, this.nativeEl, router, this.navCtrl, zone, commonLocation);
|
||||
this.parentContexts.onChildOutletCreated(this.name, this as any);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.stackCtrl.destroy();
|
||||
this.inputBinder?.unsubscribeFromRouteData(this);
|
||||
}
|
||||
|
||||
getContext(): OutletContext | null {
|
||||
return this.parentContexts.getContext(this.name);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initializeOutletWithName();
|
||||
}
|
||||
|
||||
// Note: Ionic deviates from the Angular Router implementation here
|
||||
private initializeOutletWithName() {
|
||||
if (!this.activated) {
|
||||
// If the outlet was not instantiated at the time the route got activated we need to populate
|
||||
// the outlet when it is initialized (ie inside a NgIf)
|
||||
const context = this.getContext();
|
||||
if (context?.route) {
|
||||
this.activateWith(context.route, context.injector);
|
||||
}
|
||||
}
|
||||
|
||||
new Promise((resolve) => componentOnReady(this.nativeEl, resolve)).then(() => {
|
||||
if (this._swipeGesture === undefined) {
|
||||
this.swipeGesture = this.config.getBoolean('swipeBackEnabled', (this.nativeEl as any).mode === 'ios');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get isActivated(): boolean {
|
||||
return !!this.activated;
|
||||
}
|
||||
|
||||
get component(): Record<string, unknown> {
|
||||
if (!this.activated) {
|
||||
throw new Error('Outlet is not activated');
|
||||
}
|
||||
return this.activated.instance;
|
||||
}
|
||||
|
||||
get activatedRoute(): ActivatedRoute {
|
||||
if (!this.activated) {
|
||||
throw new Error('Outlet is not activated');
|
||||
}
|
||||
return this._activatedRoute as ActivatedRoute;
|
||||
}
|
||||
|
||||
get activatedRouteData(): Data {
|
||||
if (this._activatedRoute) {
|
||||
return this._activatedRoute.snapshot.data;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the `RouteReuseStrategy` instructs to detach the subtree
|
||||
*/
|
||||
detach(): ComponentRef<any> {
|
||||
throw new Error('incompatible reuse strategy');
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the `RouteReuseStrategy` instructs to re-attach a previously detached subtree
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
attach(_ref: ComponentRef<any>, _activatedRoute: ActivatedRoute): void {
|
||||
throw new Error('incompatible reuse strategy');
|
||||
}
|
||||
|
||||
deactivate(): void {
|
||||
if (this.activated) {
|
||||
if (this.activatedView) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const context = this.getContext()!;
|
||||
this.activatedView.savedData = new Map(context.children['contexts']);
|
||||
|
||||
/**
|
||||
* Angular v11.2.10 introduced a change
|
||||
* where this route context is cleared out when
|
||||
* a router-outlet is deactivated, However,
|
||||
* we need this route information in order to
|
||||
* return a user back to the correct tab when
|
||||
* leaving and then going back to the tab context.
|
||||
*/
|
||||
const primaryOutlet = this.activatedView.savedData.get('primary');
|
||||
if (primaryOutlet && context.route) {
|
||||
primaryOutlet.route = { ...context.route };
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure we are saving the NavigationExtras
|
||||
* data otherwise it will be lost
|
||||
*/
|
||||
this.activatedView.savedExtras = {};
|
||||
if (context.route) {
|
||||
const contextSnapshot = context.route.snapshot;
|
||||
|
||||
this.activatedView.savedExtras.queryParams = contextSnapshot.queryParams;
|
||||
(this.activatedView.savedExtras.fragment as string | null) = contextSnapshot.fragment;
|
||||
}
|
||||
}
|
||||
const c = this.component;
|
||||
this.activatedView = null;
|
||||
this.activated = null;
|
||||
this._activatedRoute = null;
|
||||
this.deactivateEvents.emit(c);
|
||||
}
|
||||
}
|
||||
|
||||
activateWith(activatedRoute: ActivatedRoute, environmentInjector: EnvironmentInjector | null): void {
|
||||
if (this.isActivated) {
|
||||
throw new Error('Cannot activate an already activated outlet');
|
||||
}
|
||||
this._activatedRoute = activatedRoute;
|
||||
|
||||
let cmpRef: any;
|
||||
let enteringView = this.stackCtrl.getExistingView(activatedRoute);
|
||||
if (enteringView) {
|
||||
cmpRef = this.activated = enteringView.ref;
|
||||
const saved = enteringView.savedData;
|
||||
if (saved) {
|
||||
// self-restore
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const context = this.getContext()!;
|
||||
context.children['contexts'] = saved;
|
||||
}
|
||||
// Updated activated route proxy for this component
|
||||
this.updateActivatedRouteProxy(cmpRef.instance, activatedRoute);
|
||||
} else {
|
||||
const snapshot = (activatedRoute as any)._futureSnapshot;
|
||||
|
||||
/**
|
||||
* Angular 14 introduces a new `loadComponent` property to the route config.
|
||||
* This function will assign a `component` property to the route snapshot.
|
||||
* We check for the presence of this property to determine if the route is
|
||||
* using standalone components.
|
||||
*/
|
||||
const childContexts = this.parentContexts.getOrCreateContext(this.name).children;
|
||||
|
||||
// We create an activated route proxy object that will maintain future updates for this component
|
||||
// over its lifecycle in the stack.
|
||||
const component$ = new BehaviorSubject<any>(null);
|
||||
const activatedRouteProxy = this.createActivatedRouteProxy(component$, activatedRoute);
|
||||
|
||||
const injector = new OutletInjector(activatedRouteProxy, childContexts, this.location.injector);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const component = snapshot.routeConfig!.component ?? snapshot.component;
|
||||
|
||||
cmpRef = this.activated = this.location.createComponent(component, {
|
||||
index: this.location.length,
|
||||
injector,
|
||||
environmentInjector: environmentInjector ?? this.environmentInjector,
|
||||
});
|
||||
|
||||
// Once the component is created we can push it to our local subject supplied to the proxy
|
||||
component$.next(cmpRef.instance);
|
||||
|
||||
// Calling `markForCheck` to make sure we will run the change detection when the
|
||||
// `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component.
|
||||
enteringView = this.stackCtrl.createView(this.activated, activatedRoute);
|
||||
|
||||
// Store references to the proxy by component
|
||||
this.proxyMap.set(cmpRef.instance, activatedRouteProxy);
|
||||
this.currentActivatedRoute$.next({ component: cmpRef.instance, activatedRoute });
|
||||
}
|
||||
|
||||
this.inputBinder?.bindActivatedRouteToOutletComponent(this);
|
||||
|
||||
this.activatedView = enteringView;
|
||||
|
||||
/**
|
||||
* The top outlet is set prior to the entering view's transition completing,
|
||||
* so that when we have nested outlets (e.g. ion-tabs inside an ion-router-outlet),
|
||||
* the tabs outlet will be assigned as the top outlet when a view inside tabs is
|
||||
* activated.
|
||||
*
|
||||
* In this scenario, activeWith is called for both the tabs and the root router outlet.
|
||||
* To avoid a race condition, we assign the top outlet synchronously.
|
||||
*/
|
||||
this.navCtrl.setTopOutlet(this);
|
||||
|
||||
const leavingView = this.stackCtrl.getActiveView();
|
||||
|
||||
this.stackWillChange.emit({
|
||||
enteringView,
|
||||
tabSwitch: isTabSwitch(enteringView, leavingView),
|
||||
});
|
||||
|
||||
this.stackCtrl.setActive(enteringView).then((data) => {
|
||||
this.activateEvents.emit(cmpRef.instance);
|
||||
this.stackDidChange.emit(data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if there are pages in the stack to go back.
|
||||
*/
|
||||
canGoBack(deep = 1, stackId?: string): boolean {
|
||||
return this.stackCtrl.canGoBack(deep, stackId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves to `true` if it the outlet was able to sucessfully pop the last N pages.
|
||||
*/
|
||||
pop(deep = 1, stackId?: string): Promise<boolean> {
|
||||
return this.stackCtrl.pop(deep, stackId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of the active page of each stack.
|
||||
*/
|
||||
getLastUrl(stackId?: string): string | undefined {
|
||||
const active = this.stackCtrl.getLastUrl(stackId);
|
||||
return active ? active.url : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RouteView of the active page of each stack.
|
||||
* @internal
|
||||
*/
|
||||
getLastRouteView(stackId?: string): RouteView | undefined {
|
||||
return this.stackCtrl.getLastUrl(stackId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the root view in the tab stack.
|
||||
* @internal
|
||||
*/
|
||||
getRootView(stackId?: string): RouteView | undefined {
|
||||
return this.stackCtrl.getRootUrl(stackId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the active stack ID. In the context of ion-tabs, it means the active tab.
|
||||
*/
|
||||
getActiveStackId(): string | undefined {
|
||||
return this.stackCtrl.getActiveStackId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Since the activated route can change over the life time of a component in an ion router outlet, we create
|
||||
* a proxy so that we can update the values over time as a user navigates back to components already in the stack.
|
||||
*/
|
||||
private createActivatedRouteProxy(component$: Observable<any>, activatedRoute: ActivatedRoute): ActivatedRoute {
|
||||
const proxy: any = new ActivatedRoute();
|
||||
|
||||
proxy._futureSnapshot = (activatedRoute as any)._futureSnapshot;
|
||||
proxy._routerState = (activatedRoute as any)._routerState;
|
||||
proxy.snapshot = activatedRoute.snapshot;
|
||||
proxy.outlet = activatedRoute.outlet;
|
||||
proxy.component = activatedRoute.component;
|
||||
|
||||
// Setup wrappers for the observables so consumers don't have to worry about switching to new observables as the state updates
|
||||
(proxy as any)._paramMap = this.proxyObservable(component$, 'paramMap');
|
||||
(proxy as any)._queryParamMap = this.proxyObservable(component$, 'queryParamMap');
|
||||
proxy.url = this.proxyObservable(component$, 'url');
|
||||
proxy.params = this.proxyObservable(component$, 'params');
|
||||
proxy.queryParams = this.proxyObservable(component$, 'queryParams');
|
||||
proxy.fragment = this.proxyObservable(component$, 'fragment');
|
||||
proxy.data = this.proxyObservable(component$, 'data');
|
||||
|
||||
return proxy as ActivatedRoute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a wrapped observable that will switch to the latest activated route matched by the given component
|
||||
*/
|
||||
private proxyObservable(component$: Observable<any>, path: string): Observable<any> {
|
||||
return component$.pipe(
|
||||
// First wait until the component instance is pushed
|
||||
filter((component) => !!component),
|
||||
switchMap((component) =>
|
||||
this.currentActivatedRoute$.pipe(
|
||||
filter((current) => current !== null && current.component === component),
|
||||
switchMap((current) => current && (current.activatedRoute as any)[path]),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the activated route proxy for the given component to the new incoming router state
|
||||
*/
|
||||
private updateActivatedRouteProxy(component: any, activatedRoute: ActivatedRoute): void {
|
||||
const proxy = this.proxyMap.get(component);
|
||||
if (!proxy) {
|
||||
throw new Error(`Could not find activated route proxy for view`);
|
||||
}
|
||||
|
||||
(proxy as any)._futureSnapshot = (activatedRoute as any)._futureSnapshot;
|
||||
(proxy as any)._routerState = (activatedRoute as any)._routerState;
|
||||
proxy.snapshot = activatedRoute.snapshot;
|
||||
proxy.outlet = activatedRoute.outlet;
|
||||
proxy.component = activatedRoute.component;
|
||||
|
||||
this.currentActivatedRoute$.next({ component, activatedRoute });
|
||||
}
|
||||
}
|
||||
|
||||
class OutletInjector implements Injector {
|
||||
constructor(private route: ActivatedRoute, private childContexts: ChildrenOutletContexts, private parent: Injector) {}
|
||||
|
||||
get(token: any, notFoundValue?: any): any {
|
||||
if (token === ActivatedRoute) {
|
||||
return this.route;
|
||||
}
|
||||
|
||||
if (token === ChildrenOutletContexts) {
|
||||
return this.childContexts;
|
||||
}
|
||||
|
||||
return this.parent.get(token, notFoundValue);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: FW-4785 - Remove this once Angular 15 support is dropped
|
||||
export const INPUT_BINDER = new InjectionToken<RoutedComponentInputBinder>('');
|
||||
|
||||
/**
|
||||
* Injectable used as a tree-shakable provider for opting in to binding router data to component
|
||||
* inputs.
|
||||
*
|
||||
* The RouterOutlet registers itself with this service when an `ActivatedRoute` is attached or
|
||||
* activated. When this happens, the service subscribes to the `ActivatedRoute` observables (params,
|
||||
* queryParams, data) and sets the inputs of the component using `ComponentRef.setInput`.
|
||||
* Importantly, when an input does not have an item in the route data with a matching key, this
|
||||
* input is set to `undefined`. If it were not done this way, the previous information would be
|
||||
* retained if the data got removed from the route (i.e. if a query parameter is removed).
|
||||
*
|
||||
* The `RouterOutlet` should unregister itself when destroyed via `unsubscribeFromRouteData` so that
|
||||
* the subscriptions are cleaned up.
|
||||
*/
|
||||
@Injectable()
|
||||
export class RoutedComponentInputBinder {
|
||||
private outletDataSubscriptions = new Map<IonRouterOutlet, Subscription>();
|
||||
|
||||
bindActivatedRouteToOutletComponent(outlet: IonRouterOutlet): void {
|
||||
this.unsubscribeFromRouteData(outlet);
|
||||
this.subscribeToRouteData(outlet);
|
||||
}
|
||||
|
||||
unsubscribeFromRouteData(outlet: IonRouterOutlet): void {
|
||||
this.outletDataSubscriptions.get(outlet)?.unsubscribe();
|
||||
this.outletDataSubscriptions.delete(outlet);
|
||||
}
|
||||
|
||||
private subscribeToRouteData(outlet: IonRouterOutlet) {
|
||||
const { activatedRoute } = outlet;
|
||||
const dataSubscription = combineLatest([activatedRoute.queryParams, activatedRoute.params, activatedRoute.data])
|
||||
.pipe(
|
||||
switchMap(([queryParams, params, data], index) => {
|
||||
data = { ...queryParams, ...params, ...data };
|
||||
// Get the first result from the data subscription synchronously so it's available to
|
||||
// the component as soon as possible (and doesn't require a second change detection).
|
||||
if (index === 0) {
|
||||
return of(data);
|
||||
}
|
||||
// Promise.resolve is used to avoid synchronously writing the wrong data when
|
||||
// two of the Observables in the `combineLatest` stream emit one after
|
||||
// another.
|
||||
return Promise.resolve(data);
|
||||
})
|
||||
)
|
||||
.subscribe((data) => {
|
||||
// Outlet may have been deactivated or changed names to be associated with a different
|
||||
// route
|
||||
if (
|
||||
!outlet.isActivated ||
|
||||
!outlet.activatedComponentRef ||
|
||||
outlet.activatedRoute !== activatedRoute ||
|
||||
activatedRoute.component === null
|
||||
) {
|
||||
this.unsubscribeFromRouteData(outlet);
|
||||
return;
|
||||
}
|
||||
|
||||
const mirror = reflectComponentType(activatedRoute.component);
|
||||
if (!mirror) {
|
||||
this.unsubscribeFromRouteData(outlet);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const { templateName } of mirror.inputs) {
|
||||
outlet.activatedComponentRef.setInput(templateName, data[templateName]);
|
||||
}
|
||||
});
|
||||
|
||||
this.outletDataSubscriptions.set(outlet, dataSubscription);
|
||||
}
|
||||
}
|
||||
export class IonRouterOutlet extends IonRouterOutletBase {}
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
import {
|
||||
AfterContentChecked,
|
||||
AfterContentInit,
|
||||
Component,
|
||||
ContentChild,
|
||||
ContentChildren,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Output,
|
||||
QueryList,
|
||||
ViewChild,
|
||||
} from '@angular/core';
|
||||
import { Component, ContentChild, ContentChildren, ViewChild, QueryList } from '@angular/core';
|
||||
import { IonTabs as IonTabsBase } from '@ionic/angular/common';
|
||||
|
||||
import { NavController } from '../../providers/nav-controller';
|
||||
import { IonTabBar } from '../proxies';
|
||||
|
||||
import { IonRouterOutlet } from './ion-router-outlet';
|
||||
import { StackDidChangeEvent, StackWillChangeEvent } from './stack-utils';
|
||||
|
||||
@Component({
|
||||
selector: 'ion-tabs',
|
||||
@@ -60,172 +47,9 @@ import { StackDidChangeEvent, StackWillChangeEvent } from './stack-utils';
|
||||
],
|
||||
})
|
||||
// eslint-disable-next-line @angular-eslint/component-class-suffix
|
||||
export class IonTabs implements AfterContentInit, AfterContentChecked {
|
||||
export class IonTabs extends IonTabsBase {
|
||||
@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>;
|
||||
|
||||
/**
|
||||
* Emitted before the tab view is changed.
|
||||
*/
|
||||
@Output() ionTabsWillChange = new EventEmitter<{ tab: string }>();
|
||||
/**
|
||||
* Emitted after the tab view is changed.
|
||||
*/
|
||||
@Output() ionTabsDidChange = new EventEmitter<{ tab: string }>();
|
||||
|
||||
private tabBarSlot = 'bottom';
|
||||
|
||||
constructor(private navCtrl: NavController) {}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this.detectSlotChanges();
|
||||
}
|
||||
|
||||
ngAfterContentChecked(): void {
|
||||
this.detectSlotChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
onStackWillChange({ enteringView, tabSwitch }: StackWillChangeEvent): void {
|
||||
const stackId = enteringView.stackId;
|
||||
if (tabSwitch && stackId !== undefined) {
|
||||
this.ionTabsWillChange.emit({ tab: stackId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
onStackDidChange({ enteringView, tabSwitch }: StackDidChangeEvent): void {
|
||||
const stackId = enteringView.stackId;
|
||||
if (tabSwitch && stackId !== undefined) {
|
||||
if (this.tabBar) {
|
||||
this.tabBar.selectedTab = stackId;
|
||||
}
|
||||
this.ionTabsDidChange.emit({ tab: stackId });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When a tab button is clicked, there are several scenarios:
|
||||
* 1. If the selected tab is currently active (the tab button has been clicked
|
||||
* again), then it should go to the root view for that tab.
|
||||
*
|
||||
* a. Get the saved root view from the router outlet. If the saved root view
|
||||
* matches the tabRootUrl, set the route view to this view including the
|
||||
* navigation extras.
|
||||
* b. If the saved root view from the router outlet does
|
||||
* not match, navigate to the tabRootUrl. No navigation extras are
|
||||
* included.
|
||||
*
|
||||
* 2. If the current tab tab is not currently selected, get the last route
|
||||
* view from the router outlet.
|
||||
*
|
||||
* a. If the last route view exists, navigate to that view including any
|
||||
* navigation extras
|
||||
* b. If the last route view doesn't exist, then navigate
|
||||
* to the default tabRootUrl
|
||||
*/
|
||||
@HostListener('ionTabButtonClick', ['$event'])
|
||||
select(tabOrEvent: string | CustomEvent): Promise<boolean> | undefined {
|
||||
const isTabString = typeof tabOrEvent === 'string';
|
||||
const tab = isTabString ? tabOrEvent : (tabOrEvent as CustomEvent).detail.tab;
|
||||
const alreadySelected = this.outlet.getActiveStackId() === tab;
|
||||
const tabRootUrl = `${this.outlet.tabsPrefix}/${tab}`;
|
||||
|
||||
/**
|
||||
* If this is a nested tab, prevent the event
|
||||
* from bubbling otherwise the outer tabs
|
||||
* will respond to this event too, causing
|
||||
* the app to get directed to the wrong place.
|
||||
*/
|
||||
if (!isTabString) {
|
||||
(tabOrEvent as CustomEvent).stopPropagation();
|
||||
}
|
||||
|
||||
if (alreadySelected) {
|
||||
const activeStackId = this.outlet.getActiveStackId();
|
||||
const activeView = this.outlet.getLastRouteView(activeStackId);
|
||||
|
||||
// If on root tab, do not navigate to root tab again
|
||||
if (activeView?.url === tabRootUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rootView = this.outlet.getRootView(tab);
|
||||
const navigationExtras = rootView && tabRootUrl === rootView.url && rootView.savedExtras;
|
||||
return this.navCtrl.navigateRoot(tabRootUrl, {
|
||||
...navigationExtras,
|
||||
animated: true,
|
||||
animationDirection: 'back',
|
||||
});
|
||||
} else {
|
||||
const lastRoute = this.outlet.getLastRouteView(tab);
|
||||
/**
|
||||
* If there is a lastRoute, goto that, otherwise goto the fallback url of the
|
||||
* selected tab
|
||||
*/
|
||||
const url = lastRoute?.url || tabRootUrl;
|
||||
const navigationExtras = lastRoute?.savedExtras;
|
||||
|
||||
return this.navCtrl.navigateRoot(url, {
|
||||
...navigationExtras,
|
||||
animated: true,
|
||||
animationDirection: 'back',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import { ElementRef, Injector, Directive, EnvironmentInjector } from '@angular/core';
|
||||
|
||||
import { AngularDelegate } from '../../providers/angular-delegate';
|
||||
import { ProxyCmp, proxyOutputs } from '../angular-component-lib/utils';
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: ['animated', 'animation', 'root', 'rootParams', 'swipeGesture'],
|
||||
methods: [
|
||||
'push',
|
||||
'insert',
|
||||
'insertPages',
|
||||
'pop',
|
||||
'popTo',
|
||||
'popToRoot',
|
||||
'removeIndex',
|
||||
'setRoot',
|
||||
'setPages',
|
||||
'getActive',
|
||||
'getByIndex',
|
||||
'canGoBack',
|
||||
'getPrevious',
|
||||
],
|
||||
})
|
||||
@Directive({
|
||||
selector: 'ion-nav',
|
||||
})
|
||||
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
||||
export class NavDelegate {
|
||||
protected el: HTMLElement;
|
||||
constructor(
|
||||
ref: ElementRef,
|
||||
environmentInjector: EnvironmentInjector,
|
||||
injector: Injector,
|
||||
angularDelegate: AngularDelegate
|
||||
) {
|
||||
this.el = ref.nativeElement;
|
||||
ref.nativeElement.delegate = angularDelegate.create(environmentInjector, injector);
|
||||
proxyOutputs(this, this.el, ['ionNavDidChange', 'ionNavWillChange']);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { LocationStrategy } from '@angular/common';
|
||||
import { ElementRef, OnChanges, OnInit, Directive, HostListener, Input, Optional } from '@angular/core';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { AnimationBuilder, RouterDirection } from '@ionic/core';
|
||||
|
||||
import { NavController } from '../../providers/nav-controller';
|
||||
import { Directive } from '@angular/core';
|
||||
import {
|
||||
RouterLinkDelegateDirective as RouterLinkDelegateBase,
|
||||
RouterLinkWithHrefDelegateDirective as RouterLinkHrefDelegateBase,
|
||||
} from '@ionic/angular/common';
|
||||
|
||||
/**
|
||||
* Adds support for Ionic routing directions and animations to the base Angular router link directive.
|
||||
@@ -14,93 +13,9 @@ import { NavController } from '../../providers/nav-controller';
|
||||
@Directive({
|
||||
selector: ':not(a):not(area)[routerLink]',
|
||||
})
|
||||
export class RouterLinkDelegateDirective implements OnInit, OnChanges {
|
||||
@Input()
|
||||
routerDirection: RouterDirection = 'forward';
|
||||
|
||||
@Input()
|
||||
routerAnimation?: AnimationBuilder;
|
||||
|
||||
constructor(
|
||||
private locationStrategy: LocationStrategy,
|
||||
private navCtrl: NavController,
|
||||
private elementRef: ElementRef,
|
||||
private router: Router,
|
||||
@Optional() private routerLink?: RouterLink
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.updateTargetUrlAndHref();
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.updateTargetUrlAndHref();
|
||||
}
|
||||
|
||||
private updateTargetUrlAndHref() {
|
||||
if (this.routerLink?.urlTree) {
|
||||
const href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.routerLink.urlTree));
|
||||
this.elementRef.nativeElement.href = href;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@HostListener('click', ['$event'])
|
||||
onClick(ev: UIEvent): void {
|
||||
this.navCtrl.setDirection(this.routerDirection, undefined, undefined, this.routerAnimation);
|
||||
|
||||
/**
|
||||
* This prevents the browser from
|
||||
* performing a page reload when pressing
|
||||
* an Ionic component with routerLink.
|
||||
* The page reload interferes with routing
|
||||
* and causes ion-back-button to disappear
|
||||
* since the local history is wiped on reload.
|
||||
*/
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
export class RouterLinkDelegateDirective extends RouterLinkDelegateBase {}
|
||||
|
||||
@Directive({
|
||||
selector: 'a[routerLink],area[routerLink]',
|
||||
})
|
||||
export class RouterLinkWithHrefDelegateDirective implements OnInit, OnChanges {
|
||||
@Input()
|
||||
routerDirection: RouterDirection = 'forward';
|
||||
|
||||
@Input()
|
||||
routerAnimation?: AnimationBuilder;
|
||||
|
||||
constructor(
|
||||
private locationStrategy: LocationStrategy,
|
||||
private navCtrl: NavController,
|
||||
private elementRef: ElementRef,
|
||||
private router: Router,
|
||||
@Optional() private routerLink?: RouterLink
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.updateTargetUrlAndHref();
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.updateTargetUrlAndHref();
|
||||
}
|
||||
|
||||
private updateTargetUrlAndHref() {
|
||||
if (this.routerLink?.urlTree) {
|
||||
const href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.routerLink.urlTree));
|
||||
this.elementRef.nativeElement.href = href;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@HostListener('click')
|
||||
onClick(): void {
|
||||
this.navCtrl.setDirection(this.routerDirection, undefined, undefined, this.routerAnimation);
|
||||
}
|
||||
}
|
||||
export class RouterLinkWithHrefDelegateDirective extends RouterLinkHrefDelegateBase {}
|
||||
|
||||
@@ -1,139 +1,11 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ContentChild,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
NgZone,
|
||||
TemplateRef,
|
||||
} from '@angular/core';
|
||||
import { ProxyCmp, proxyOutputs } from '../angular-component-lib/utils';
|
||||
import { Components, ModalBreakpointChangeEventDetail } from '@ionic/core';
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { IonModal as IonModalBase } from '@ionic/angular/common';
|
||||
|
||||
export declare interface IonModal extends Components.IonModal {
|
||||
/**
|
||||
* Emitted after the modal has presented.
|
||||
**/
|
||||
ionModalDidPresent: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted before the modal has presented.
|
||||
*/
|
||||
ionModalWillPresent: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted before the modal has dismissed.
|
||||
*/
|
||||
ionModalWillDismiss: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted after the modal has dismissed.
|
||||
*/
|
||||
ionModalDidDismiss: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted after the modal breakpoint has changed.
|
||||
*/
|
||||
ionBreakpointDidChange: EventEmitter<CustomEvent<ModalBreakpointChangeEventDetail>>;
|
||||
/**
|
||||
* Emitted after the modal has presented. Shorthand for ionModalWillDismiss.
|
||||
*/
|
||||
didPresent: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted before the modal has presented. Shorthand for ionModalWillPresent.
|
||||
*/
|
||||
willPresent: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted before the modal has dismissed. Shorthand for ionModalWillDismiss.
|
||||
*/
|
||||
willDismiss: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted after the modal has dismissed. Shorthand for ionModalDidDismiss.
|
||||
*/
|
||||
didDismiss: EventEmitter<CustomEvent>;
|
||||
}
|
||||
@ProxyCmp({
|
||||
inputs: [
|
||||
'animated',
|
||||
'keepContentsMounted',
|
||||
'backdropBreakpoint',
|
||||
'backdropDismiss',
|
||||
'breakpoints',
|
||||
'canDismiss',
|
||||
'cssClass',
|
||||
'enterAnimation',
|
||||
'event',
|
||||
'handle',
|
||||
'handleBehavior',
|
||||
'initialBreakpoint',
|
||||
'isOpen',
|
||||
'keyboardClose',
|
||||
'leaveAnimation',
|
||||
'mode',
|
||||
'presentingElement',
|
||||
'showBackdrop',
|
||||
'translucent',
|
||||
'trigger',
|
||||
],
|
||||
methods: ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss', 'setCurrentBreakpoint', 'getCurrentBreakpoint'],
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-modal',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `<div class="ion-delegate-host ion-page" *ngIf="isCmpOpen || keepContentsMounted">
|
||||
<ng-container [ngTemplateOutlet]="template"></ng-container>
|
||||
</div>`,
|
||||
inputs: [
|
||||
'animated',
|
||||
'keepContentsMounted',
|
||||
'backdropBreakpoint',
|
||||
'backdropDismiss',
|
||||
'breakpoints',
|
||||
'canDismiss',
|
||||
'cssClass',
|
||||
'enterAnimation',
|
||||
'event',
|
||||
'handle',
|
||||
'handleBehavior',
|
||||
'initialBreakpoint',
|
||||
'isOpen',
|
||||
'keyboardClose',
|
||||
'leaveAnimation',
|
||||
'mode',
|
||||
'presentingElement',
|
||||
'showBackdrop',
|
||||
'translucent',
|
||||
'trigger',
|
||||
],
|
||||
})
|
||||
export class IonModal {
|
||||
// TODO(FW-2827): type
|
||||
@ContentChild(TemplateRef, { static: false }) template: TemplateRef<any>;
|
||||
|
||||
isCmpOpen: boolean = false;
|
||||
|
||||
protected el: HTMLElement;
|
||||
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
||||
this.el = r.nativeElement;
|
||||
|
||||
this.el.addEventListener('ionMount', () => {
|
||||
this.isCmpOpen = true;
|
||||
c.detectChanges();
|
||||
});
|
||||
this.el.addEventListener('didDismiss', () => {
|
||||
this.isCmpOpen = false;
|
||||
c.detectChanges();
|
||||
});
|
||||
proxyOutputs(this, this.el, [
|
||||
'ionModalDidPresent',
|
||||
'ionModalWillPresent',
|
||||
'ionModalWillDismiss',
|
||||
'ionModalDidDismiss',
|
||||
'ionBreakpointDidChange',
|
||||
'didPresent',
|
||||
'willPresent',
|
||||
'willDismiss',
|
||||
'didDismiss',
|
||||
]);
|
||||
}
|
||||
}
|
||||
export class IonModal extends IonModalBase {}
|
||||
|
||||
@@ -1,131 +1,9 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ContentChild,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
NgZone,
|
||||
TemplateRef,
|
||||
} from '@angular/core';
|
||||
import { ProxyCmp, proxyOutputs } from '../angular-component-lib/utils';
|
||||
import { Components } from '@ionic/core';
|
||||
export declare interface IonPopover extends Components.IonPopover {
|
||||
/**
|
||||
* Emitted after the popover has presented.
|
||||
*/
|
||||
ionPopoverDidPresent: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted before the popover has presented.
|
||||
*/
|
||||
ionPopoverWillPresent: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted after the popover has dismissed.
|
||||
*/
|
||||
ionPopoverWillDismiss: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted after the popover has dismissed.
|
||||
*/
|
||||
ionPopoverDidDismiss: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted after the popover has presented. Shorthand for ionPopoverWillDismiss.
|
||||
*/
|
||||
didPresent: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted before the popover has presented. Shorthand for ionPopoverWillPresent.
|
||||
*/
|
||||
willPresent: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted after the popover has presented. Shorthand for ionPopoverWillDismiss.
|
||||
*/
|
||||
willDismiss: EventEmitter<CustomEvent>;
|
||||
/**
|
||||
* Emitted after the popover has dismissed. Shorthand for ionPopoverDidDismiss.
|
||||
*/
|
||||
didDismiss: EventEmitter<CustomEvent>;
|
||||
}
|
||||
@ProxyCmp({
|
||||
inputs: [
|
||||
'alignment',
|
||||
'animated',
|
||||
'arrow',
|
||||
'keepContentsMounted',
|
||||
'backdropDismiss',
|
||||
'cssClass',
|
||||
'dismissOnSelect',
|
||||
'enterAnimation',
|
||||
'event',
|
||||
'isOpen',
|
||||
'keyboardClose',
|
||||
'leaveAnimation',
|
||||
'mode',
|
||||
'showBackdrop',
|
||||
'translucent',
|
||||
'trigger',
|
||||
'triggerAction',
|
||||
'reference',
|
||||
'size',
|
||||
'side',
|
||||
],
|
||||
methods: ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss'],
|
||||
})
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { IonPopover as IonPopoverBase } from '@ionic/angular/common';
|
||||
|
||||
@Component({
|
||||
selector: 'ion-popover',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `<ng-container [ngTemplateOutlet]="template" *ngIf="isCmpOpen || keepContentsMounted"></ng-container>`,
|
||||
inputs: [
|
||||
'alignment',
|
||||
'animated',
|
||||
'arrow',
|
||||
'keepContentsMounted',
|
||||
'backdropDismiss',
|
||||
'cssClass',
|
||||
'dismissOnSelect',
|
||||
'enterAnimation',
|
||||
'event',
|
||||
'isOpen',
|
||||
'keyboardClose',
|
||||
'leaveAnimation',
|
||||
'mode',
|
||||
'showBackdrop',
|
||||
'translucent',
|
||||
'trigger',
|
||||
'triggerAction',
|
||||
'reference',
|
||||
'size',
|
||||
'side',
|
||||
],
|
||||
})
|
||||
export class IonPopover {
|
||||
// TODO(FW-2827): type
|
||||
@ContentChild(TemplateRef, { static: false }) template: TemplateRef<any>;
|
||||
|
||||
isCmpOpen: boolean = false;
|
||||
|
||||
protected el: HTMLElement;
|
||||
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
||||
this.el = r.nativeElement;
|
||||
|
||||
this.el.addEventListener('ionMount', () => {
|
||||
this.isCmpOpen = true;
|
||||
c.detectChanges();
|
||||
});
|
||||
this.el.addEventListener('didDismiss', () => {
|
||||
this.isCmpOpen = false;
|
||||
c.detectChanges();
|
||||
});
|
||||
proxyOutputs(this, this.el, [
|
||||
'ionPopoverDidPresent',
|
||||
'ionPopoverWillPresent',
|
||||
'ionPopoverWillDismiss',
|
||||
'ionPopoverDidDismiss',
|
||||
'didPresent',
|
||||
'willPresent',
|
||||
'willDismiss',
|
||||
'didDismiss',
|
||||
]);
|
||||
}
|
||||
}
|
||||
export class IonPopover extends IonPopoverBase {}
|
||||
|
||||
@@ -8,7 +8,6 @@ export const DIRECTIVES = [
|
||||
d.IonAlert,
|
||||
d.IonApp,
|
||||
d.IonAvatar,
|
||||
d.IonBackButton,
|
||||
d.IonBackdrop,
|
||||
d.IonBadge,
|
||||
d.IonBreadcrumb,
|
||||
@@ -50,7 +49,6 @@ export const DIRECTIVES = [
|
||||
d.IonMenu,
|
||||
d.IonMenuButton,
|
||||
d.IonMenuToggle,
|
||||
d.IonNav,
|
||||
d.IonNavLink,
|
||||
d.IonNote,
|
||||
d.IonPicker,
|
||||
|
||||
@@ -230,28 +230,6 @@ export class IonAvatar {
|
||||
export declare interface IonAvatar extends Components.IonAvatar {}
|
||||
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: ['color', 'defaultHref', 'disabled', 'icon', 'mode', 'routerAnimation', 'text', 'type']
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-back-button',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['color', 'defaultHref', 'disabled', 'icon', 'mode', 'routerAnimation', 'text', 'type'],
|
||||
})
|
||||
export class IonBackButton {
|
||||
protected el: HTMLElement;
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
||||
c.detach();
|
||||
this.el = r.nativeElement;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export declare interface IonBackButton extends Components.IonBackButton {}
|
||||
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: ['stopPropagation', 'tappable', 'visible']
|
||||
})
|
||||
@@ -1395,39 +1373,6 @@ export class IonMenuToggle {
|
||||
export declare interface IonMenuToggle extends Components.IonMenuToggle {}
|
||||
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: ['animated', 'animation', 'root', 'rootParams', 'swipeGesture'],
|
||||
methods: ['push', 'insert', 'insertPages', 'pop', 'popTo', 'popToRoot', 'removeIndex', 'setRoot', 'setPages', 'getActive', 'getByIndex', 'canGoBack', 'getPrevious']
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-nav',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['animated', 'animation', 'root', 'rootParams', 'swipeGesture'],
|
||||
})
|
||||
export class IonNav {
|
||||
protected el: HTMLElement;
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
||||
c.detach();
|
||||
this.el = r.nativeElement;
|
||||
proxyOutputs(this, this.el, ['ionNavWillChange', 'ionNavDidChange']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export declare interface IonNav extends Components.IonNav {
|
||||
/**
|
||||
* Event fired when the nav will change components
|
||||
*/
|
||||
ionNavWillChange: EventEmitter<CustomEvent<void>>;
|
||||
/**
|
||||
* Event fired when the nav has changed components
|
||||
*/
|
||||
ionNavDidChange: EventEmitter<CustomEvent<void>>;
|
||||
}
|
||||
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: ['component', 'componentProps', 'routerAnimation', 'routerDirection']
|
||||
})
|
||||
|
||||
@@ -5,39 +5,39 @@ export { RadioValueAccessorDirective as RadioValueAccessor } from './directives/
|
||||
export { SelectValueAccessorDirective as SelectValueAccessor } from './directives/control-value-accessors/select-value-accessor';
|
||||
export { TextValueAccessorDirective as TextValueAccessor } from './directives/control-value-accessors/text-value-accessor';
|
||||
export { IonTabs } from './directives/navigation/ion-tabs';
|
||||
export { IonBackButtonDelegateDirective as IonBackButtonDelegate } from './directives/navigation/ion-back-button';
|
||||
export { NavDelegate } from './directives/navigation/nav-delegate';
|
||||
export { IonBackButton } from './directives/navigation/ion-back-button';
|
||||
export { IonNav } from './directives/navigation/ion-nav';
|
||||
export { IonRouterOutlet } from './directives/navigation/ion-router-outlet';
|
||||
export {
|
||||
RouterLinkDelegateDirective as RouterLinkDelegate,
|
||||
RouterLinkWithHrefDelegateDirective as RouterLinkWithHrefDelegate,
|
||||
} from './directives/navigation/router-link-delegate';
|
||||
|
||||
export { NavParams } from './directives/navigation/nav-params';
|
||||
export { IonModal } from './directives/overlays/modal';
|
||||
export { IonPopover } from './directives/overlays/popover';
|
||||
export * from './directives/proxies';
|
||||
export * from './directives/validators';
|
||||
|
||||
// PROVIDERS
|
||||
export { AngularDelegate } from './providers/angular-delegate';
|
||||
export { ActionSheetController } from './providers/action-sheet-controller';
|
||||
export { AlertController } from './providers/alert-controller';
|
||||
export { LoadingController } from './providers/loading-controller';
|
||||
export { MenuController } from './providers/menu-controller';
|
||||
export { PickerController } from './providers/picker-controller';
|
||||
export { ModalController } from './providers/modal-controller';
|
||||
export { Platform } from './providers/platform';
|
||||
export { PopoverController } from './providers/popover-controller';
|
||||
export { ToastController } from './providers/toast-controller';
|
||||
export { NavController } from './providers/nav-controller';
|
||||
export { DomController } from './providers/dom-controller';
|
||||
export { Config } from './providers/config';
|
||||
export { AnimationController } from './providers/animation-controller';
|
||||
export { GestureController } from './providers/gesture-controller';
|
||||
|
||||
// ROUTER STRATEGY
|
||||
export { IonicRouteStrategy } from './util/ionic-router-reuse-strategy';
|
||||
export {
|
||||
ActionSheetController,
|
||||
AlertController,
|
||||
LoadingController,
|
||||
MenuController,
|
||||
ModalController,
|
||||
PickerController,
|
||||
PopoverController,
|
||||
ToastController,
|
||||
AnimationController,
|
||||
GestureController,
|
||||
DomController,
|
||||
NavController,
|
||||
Config,
|
||||
Platform,
|
||||
AngularDelegate,
|
||||
NavParams,
|
||||
IonicRouteStrategy,
|
||||
} from '@ionic/angular/common';
|
||||
|
||||
// TYPES
|
||||
export * from './types/ionic-lifecycle-hooks';
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { CommonModule, DOCUMENT } from '@angular/common';
|
||||
import { ModuleWithProviders, APP_INITIALIZER, NgModule, NgZone } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
ModalController,
|
||||
PopoverController,
|
||||
ConfigToken,
|
||||
AngularDelegate,
|
||||
provideComponentInputBinding,
|
||||
} from '@ionic/angular/common';
|
||||
import { IonicConfig } from '@ionic/core';
|
||||
|
||||
import { appInitialize } from './app-initialize';
|
||||
@@ -11,10 +17,10 @@ import {
|
||||
SelectValueAccessorDirective,
|
||||
TextValueAccessorDirective,
|
||||
} from './directives/control-value-accessors';
|
||||
import { IonBackButtonDelegateDirective } from './directives/navigation/ion-back-button';
|
||||
import { INPUT_BINDER, IonRouterOutlet, RoutedComponentInputBinder } from './directives/navigation/ion-router-outlet';
|
||||
import { IonBackButton } from './directives/navigation/ion-back-button';
|
||||
import { IonNav } from './directives/navigation/ion-nav';
|
||||
import { IonRouterOutlet } from './directives/navigation/ion-router-outlet';
|
||||
import { IonTabs } from './directives/navigation/ion-tabs';
|
||||
import { NavDelegate } from './directives/navigation/nav-delegate';
|
||||
import {
|
||||
RouterLinkDelegateDirective,
|
||||
RouterLinkWithHrefDelegateDirective,
|
||||
@@ -23,10 +29,6 @@ import { IonModal } from './directives/overlays/modal';
|
||||
import { IonPopover } from './directives/overlays/popover';
|
||||
import { DIRECTIVES } from './directives/proxies-list';
|
||||
import { IonMaxValidator, IonMinValidator } from './directives/validators';
|
||||
import { AngularDelegate } from './providers/angular-delegate';
|
||||
import { ConfigToken } from './providers/config';
|
||||
import { ModalController } from './providers/modal-controller';
|
||||
import { PopoverController } from './providers/popover-controller';
|
||||
|
||||
const DECLARATIONS = [
|
||||
// generated proxies
|
||||
@@ -46,8 +48,8 @@ const DECLARATIONS = [
|
||||
// navigation
|
||||
IonTabs,
|
||||
IonRouterOutlet,
|
||||
IonBackButtonDelegateDirective,
|
||||
NavDelegate,
|
||||
IonBackButton,
|
||||
IonNav,
|
||||
RouterLinkDelegateDirective,
|
||||
RouterLinkWithHrefDelegateDirective,
|
||||
|
||||
@@ -77,23 +79,8 @@ export class IonicModule {
|
||||
multi: true,
|
||||
deps: [ConfigToken, DOCUMENT, NgZone],
|
||||
},
|
||||
{
|
||||
provide: INPUT_BINDER,
|
||||
useFactory: componentInputBindingFactory,
|
||||
deps: [Router],
|
||||
},
|
||||
provideComponentInputBinding(),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function componentInputBindingFactory(router?: Router) {
|
||||
/**
|
||||
* We cast the router to any here, since the componentInputBindingEnabled
|
||||
* property is not available until Angular v16.
|
||||
*/
|
||||
if ((router as any)?.componentInputBindingEnabled) {
|
||||
return new RoutedComponentInputBinder();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
5
packages/angular/standalone/ng-package.json
Normal file
5
packages/angular/standalone/ng-package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"lib": {
|
||||
"entryFile": "src/index.ts"
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/* eslint-disable */
|
||||
/* tslint:disable */
|
||||
import { fromEvent } from 'rxjs';
|
||||
|
||||
export const proxyInputs = (Cmp: any, inputs: string[]) => {
|
||||
const Prototype = Cmp.prototype;
|
||||
inputs.forEach((item) => {
|
||||
Object.defineProperty(Prototype, item, {
|
||||
get() {
|
||||
return this.el[item];
|
||||
},
|
||||
set(val: any) {
|
||||
this.z.runOutsideAngular(() => (this.el[item] = val));
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const proxyMethods = (Cmp: any, methods: string[]) => {
|
||||
const Prototype = Cmp.prototype;
|
||||
methods.forEach((methodName) => {
|
||||
Prototype[methodName] = function () {
|
||||
const args = arguments;
|
||||
return this.z.runOutsideAngular(() => this.el[methodName].apply(this.el, args));
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const proxyOutputs = (instance: any, el: any, events: string[]) => {
|
||||
events.forEach((eventName) => (instance[eventName] = fromEvent(el, eventName)));
|
||||
};
|
||||
|
||||
export const defineCustomElement = (tagName: string, customElement: any) => {
|
||||
if (customElement !== undefined && typeof customElements !== 'undefined' && !customElements.get(tagName)) {
|
||||
customElements.define(tagName, customElement);
|
||||
}
|
||||
};
|
||||
|
||||
// tslint:disable-next-line: only-arrow-functions
|
||||
export function ProxyCmp(opts: { defineCustomElementFn?: () => void; inputs?: any; methods?: any }) {
|
||||
const decorator = function (cls: any) {
|
||||
const { defineCustomElementFn, inputs, methods } = opts;
|
||||
|
||||
if (defineCustomElementFn !== undefined) {
|
||||
defineCustomElementFn();
|
||||
}
|
||||
|
||||
if (inputs) {
|
||||
proxyInputs(cls, inputs);
|
||||
}
|
||||
if (methods) {
|
||||
proxyMethods(cls, methods);
|
||||
}
|
||||
return cls;
|
||||
};
|
||||
return decorator;
|
||||
}
|
||||
86
packages/angular/standalone/src/directives/checkbox.ts
Normal file
86
packages/angular/standalone/src/directives/checkbox.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Injector,
|
||||
NgZone,
|
||||
} from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { ValueAccessor, setIonicClasses } from '@ionic/angular/common';
|
||||
import type { CheckboxChangeEventDetail, Components } from '@ionic/core/components';
|
||||
import { defineCustomElement } from '@ionic/core/components/ion-checkbox.js';
|
||||
|
||||
import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';
|
||||
|
||||
const CHECKBOX_INPUTS = [
|
||||
'checked',
|
||||
'color',
|
||||
'disabled',
|
||||
'indeterminate',
|
||||
'justify',
|
||||
'labelPlacement',
|
||||
'legacy',
|
||||
'mode',
|
||||
'name',
|
||||
'value',
|
||||
];
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineCustomElement,
|
||||
inputs: CHECKBOX_INPUTS,
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-checkbox',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: CHECKBOX_INPUTS,
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: IonCheckbox,
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
export class IonCheckbox extends ValueAccessor {
|
||||
protected el: HTMLElement;
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) {
|
||||
super(injector, r);
|
||||
c.detach();
|
||||
this.el = r.nativeElement;
|
||||
proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur']);
|
||||
}
|
||||
|
||||
writeValue(value: boolean): void {
|
||||
this.elementRef.nativeElement.checked = this.lastValue = value;
|
||||
setIonicClasses(this.elementRef);
|
||||
}
|
||||
|
||||
@HostListener('ionChange', ['$event.target'])
|
||||
handleIonChange(el: HTMLIonCheckboxElement | HTMLIonToggleElement): void {
|
||||
this.handleValueChange(el, el.checked);
|
||||
}
|
||||
}
|
||||
|
||||
export declare interface IonCheckbox extends Components.IonCheckbox {
|
||||
/**
|
||||
* Emitted when the checked property has changed
|
||||
as a result of a user action such as a click.
|
||||
This event will not emit when programmatically
|
||||
setting the checked property.
|
||||
*/
|
||||
ionChange: EventEmitter<CustomEvent<CheckboxChangeEventDetail>>;
|
||||
/**
|
||||
* Emitted when the checkbox has focus.
|
||||
*/
|
||||
ionFocus: EventEmitter<CustomEvent<void>>;
|
||||
/**
|
||||
* Emitted when the checkbox loses focus.
|
||||
*/
|
||||
ionBlur: EventEmitter<CustomEvent<void>>;
|
||||
}
|
||||
103
packages/angular/standalone/src/directives/datetime.ts
Normal file
103
packages/angular/standalone/src/directives/datetime.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Injector,
|
||||
NgZone,
|
||||
} from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { ValueAccessor } from '@ionic/angular/common';
|
||||
import type { DatetimeChangeEventDetail, Components } from '@ionic/core/components';
|
||||
import { defineCustomElement } from '@ionic/core/components/ion-datetime.js';
|
||||
|
||||
import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';
|
||||
|
||||
const DATETIME_INPUTS = [
|
||||
'cancelText',
|
||||
'clearText',
|
||||
'color',
|
||||
'dayValues',
|
||||
'disabled',
|
||||
'doneText',
|
||||
'firstDayOfWeek',
|
||||
'highlightedDates',
|
||||
'hourCycle',
|
||||
'hourValues',
|
||||
'isDateEnabled',
|
||||
'locale',
|
||||
'max',
|
||||
'min',
|
||||
'minuteValues',
|
||||
'mode',
|
||||
'monthValues',
|
||||
'multiple',
|
||||
'name',
|
||||
'preferWheel',
|
||||
'presentation',
|
||||
'readonly',
|
||||
'showClearButton',
|
||||
'showDefaultButtons',
|
||||
'showDefaultTimeLabel',
|
||||
'showDefaultTitle',
|
||||
'size',
|
||||
'titleSelectedDatesFormatter',
|
||||
'value',
|
||||
'yearValues',
|
||||
];
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineCustomElement,
|
||||
inputs: DATETIME_INPUTS,
|
||||
methods: ['confirm', 'reset', 'cancel'],
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-datetime',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: DATETIME_INPUTS,
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: IonDatetime,
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
export class IonDatetime extends ValueAccessor {
|
||||
protected el: HTMLElement;
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) {
|
||||
super(injector, r);
|
||||
c.detach();
|
||||
this.el = r.nativeElement;
|
||||
proxyOutputs(this, this.el, ['ionCancel', 'ionChange', 'ionFocus', 'ionBlur']);
|
||||
}
|
||||
|
||||
@HostListener('ionChange', ['$event.target'])
|
||||
handleIonChange(el: HTMLIonDatetimeElement): void {
|
||||
this.handleValueChange(el, el.value);
|
||||
}
|
||||
}
|
||||
|
||||
export declare interface IonDatetime extends Components.IonDatetime {
|
||||
/**
|
||||
* Emitted when the datetime selection was cancelled.
|
||||
*/
|
||||
ionCancel: EventEmitter<CustomEvent<void>>;
|
||||
/**
|
||||
* Emitted when the value (selected date) has changed.
|
||||
*/
|
||||
ionChange: EventEmitter<CustomEvent<DatetimeChangeEventDetail>>;
|
||||
/**
|
||||
* Emitted when the datetime has focus.
|
||||
*/
|
||||
ionFocus: EventEmitter<CustomEvent<void>>;
|
||||
/**
|
||||
* Emitted when the datetime loses focus.
|
||||
*/
|
||||
ionBlur: EventEmitter<CustomEvent<void>>;
|
||||
}
|
||||
24
packages/angular/standalone/src/directives/icon.ts
Normal file
24
packages/angular/standalone/src/directives/icon.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core';
|
||||
import { defineCustomElement as defineIonIcon } from 'ionicons/components/ion-icon.js';
|
||||
|
||||
import { ProxyCmp } from './angular-component-lib/utils';
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineIonIcon,
|
||||
inputs: ['color', 'flipRtl', 'icon', 'ios', 'lazy', 'md', 'mode', 'name', 'sanitize', 'size', 'src'],
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-icon',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: ['color', 'flipRtl', 'icon', 'ios', 'lazy', 'md', 'mode', 'name', 'sanitize', 'size', 'src'],
|
||||
standalone: true,
|
||||
})
|
||||
export class IonIcon {
|
||||
protected el: HTMLElement;
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
|
||||
c.detach();
|
||||
this.el = r.nativeElement;
|
||||
}
|
||||
}
|
||||
12
packages/angular/standalone/src/directives/index.ts
Normal file
12
packages/angular/standalone/src/directives/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export * from './checkbox';
|
||||
export * from './datetime';
|
||||
export * from './icon';
|
||||
export * from './input';
|
||||
export * from './radio-group';
|
||||
export * from './radio';
|
||||
export * from './range';
|
||||
export * from './searchbar';
|
||||
export * from './segment';
|
||||
export * from './select';
|
||||
export * from './textarea';
|
||||
export * from './toggle';
|
||||
145
packages/angular/standalone/src/directives/input.ts
Normal file
145
packages/angular/standalone/src/directives/input.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Injector,
|
||||
NgZone,
|
||||
} from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { ValueAccessor } from '@ionic/angular/common';
|
||||
import type {
|
||||
InputInputEventDetail as IIonInputInputInputEventDetail,
|
||||
InputChangeEventDetail as IIonInputInputChangeEventDetail,
|
||||
Components,
|
||||
} from '@ionic/core/components';
|
||||
import { defineCustomElement } from '@ionic/core/components/ion-input.js';
|
||||
|
||||
import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';
|
||||
|
||||
const INPUT_INPUTS = [
|
||||
'accept',
|
||||
'autocapitalize',
|
||||
'autocomplete',
|
||||
'autocorrect',
|
||||
'autofocus',
|
||||
'clearInput',
|
||||
'clearOnEdit',
|
||||
'color',
|
||||
'counter',
|
||||
'counterFormatter',
|
||||
'debounce',
|
||||
'disabled',
|
||||
'enterkeyhint',
|
||||
'errorText',
|
||||
'fill',
|
||||
'helperText',
|
||||
'inputmode',
|
||||
'label',
|
||||
'labelPlacement',
|
||||
'legacy',
|
||||
'max',
|
||||
'maxlength',
|
||||
'min',
|
||||
'minlength',
|
||||
'mode',
|
||||
'multiple',
|
||||
'name',
|
||||
'pattern',
|
||||
'placeholder',
|
||||
'readonly',
|
||||
'required',
|
||||
'shape',
|
||||
'size',
|
||||
'spellcheck',
|
||||
'step',
|
||||
'type',
|
||||
'value',
|
||||
];
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineCustomElement,
|
||||
inputs: INPUT_INPUTS,
|
||||
methods: ['setFocus', 'getInputElement'],
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-input',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: INPUT_INPUTS,
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: IonInput,
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
export class IonInput extends ValueAccessor {
|
||||
protected el: HTMLElement;
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) {
|
||||
super(injector, r);
|
||||
c.detach();
|
||||
this.el = r.nativeElement;
|
||||
proxyOutputs(this, this.el, ['ionInput', 'ionChange', 'ionBlur', 'ionFocus']);
|
||||
}
|
||||
|
||||
@HostListener('ionInput', ['$event.target'])
|
||||
handleIonInput(el: HTMLIonInputElement): void {
|
||||
this.handleValueChange(el, el.value);
|
||||
}
|
||||
|
||||
registerOnChange(fn: (_: any) => void): void {
|
||||
super.registerOnChange((value: string) => {
|
||||
if (this.type === 'number') {
|
||||
/**
|
||||
* If the input type is `number`, we need to convert the value to a number
|
||||
* when the value is not empty. If the value is empty, we want to treat
|
||||
* the value as null.
|
||||
*/
|
||||
fn(value === '' ? null : parseFloat(value));
|
||||
} else {
|
||||
fn(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export declare interface IonInput extends Components.IonInput {
|
||||
/**
|
||||
* The `ionInput` event is fired each time the user modifies the input's value.
|
||||
Unlike the `ionChange` event, the `ionInput` event is fired for each alteration
|
||||
to the input's value. This typically happens for each keystroke as the user types.
|
||||
|
||||
For elements that accept text input (`type=text`, `type=tel`, etc.), the interface
|
||||
is [`InputEvent`](https://developer.mozilla.org/en-US/docs/Web/API/InputEvent); for others,
|
||||
the interface is [`Event`](https://developer.mozilla.org/en-US/docs/Web/API/Event). If
|
||||
the input is cleared on edit, the type is `null`.
|
||||
*/
|
||||
ionInput: EventEmitter<CustomEvent<IIonInputInputInputEventDetail>>;
|
||||
/**
|
||||
* The `ionChange` event is fired when the user modifies the input's value.
|
||||
Unlike the `ionInput` event, the `ionChange` event is only fired when changes
|
||||
are committed, not as the user types.
|
||||
|
||||
Depending on the way the users interacts with the element, the `ionChange`
|
||||
event fires at a different moment:
|
||||
- When the user commits the change explicitly (e.g. by selecting a date
|
||||
from a date picker for `<ion-input type="date">`, pressing the "Enter" key, etc.).
|
||||
- When the element loses focus after its value has changed: for elements
|
||||
where the user's interaction is typing.
|
||||
*/
|
||||
ionChange: EventEmitter<CustomEvent<IIonInputInputChangeEventDetail>>;
|
||||
/**
|
||||
* Emitted when the input loses focus.
|
||||
*/
|
||||
ionBlur: EventEmitter<CustomEvent<FocusEvent>>;
|
||||
/**
|
||||
* Emitted when the input has focus.
|
||||
*/
|
||||
ionFocus: EventEmitter<CustomEvent<FocusEvent>>;
|
||||
}
|
||||
2286
packages/angular/standalone/src/directives/proxies.ts
Normal file
2286
packages/angular/standalone/src/directives/proxies.ts
Normal file
File diff suppressed because it is too large
Load Diff
59
packages/angular/standalone/src/directives/radio-group.ts
Normal file
59
packages/angular/standalone/src/directives/radio-group.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Injector,
|
||||
NgZone,
|
||||
} from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { ValueAccessor } from '@ionic/angular/common';
|
||||
import type { RadioGroupChangeEventDetail, Components } from '@ionic/core/components';
|
||||
import { defineCustomElement } from '@ionic/core/components/ion-radio-group.js';
|
||||
|
||||
import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';
|
||||
|
||||
const RADIO_GROUP_INPUTS = ['allowEmptySelection', 'name', 'value'];
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineCustomElement,
|
||||
inputs: RADIO_GROUP_INPUTS,
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-radio-group',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: RADIO_GROUP_INPUTS,
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: IonRadioGroup,
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
export class IonRadioGroup extends ValueAccessor {
|
||||
protected el: HTMLElement;
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) {
|
||||
super(injector, r);
|
||||
c.detach();
|
||||
this.el = r.nativeElement;
|
||||
proxyOutputs(this, this.el, ['ionChange']);
|
||||
}
|
||||
|
||||
@HostListener('ionChange', ['$event.target'])
|
||||
handleIonChange(el: HTMLIonRadioGroupElement): void {
|
||||
this.handleValueChange(el, el.value);
|
||||
}
|
||||
}
|
||||
|
||||
export declare interface IonRadioGroup extends Components.IonRadioGroup {
|
||||
/**
|
||||
* Emitted when the value has changed.
|
||||
*/
|
||||
ionChange: EventEmitter<CustomEvent<RadioGroupChangeEventDetail>>;
|
||||
}
|
||||
67
packages/angular/standalone/src/directives/radio.ts
Normal file
67
packages/angular/standalone/src/directives/radio.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Injector,
|
||||
NgZone,
|
||||
} from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { ValueAccessor } from '@ionic/angular/common';
|
||||
import type { Components } from '@ionic/core/components';
|
||||
import { defineCustomElement } from '@ionic/core/components/ion-radio.js';
|
||||
|
||||
import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';
|
||||
|
||||
const RADIO_INPUTS = ['color', 'disabled', 'justify', 'labelPlacement', 'legacy', 'mode', 'name', 'value'];
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineCustomElement,
|
||||
inputs: RADIO_INPUTS,
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-radio',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: RADIO_INPUTS,
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: IonRadio,
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
export class IonRadio extends ValueAccessor {
|
||||
protected el: HTMLElement;
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) {
|
||||
super(injector, r);
|
||||
c.detach();
|
||||
this.el = r.nativeElement;
|
||||
proxyOutputs(this, this.el, ['ionFocus', 'ionBlur']);
|
||||
}
|
||||
|
||||
@HostListener('ionSelect', ['$event.target'])
|
||||
handleIonSelect(el: any): void {
|
||||
/**
|
||||
* The `el` type is any to access the `checked` state property
|
||||
* that is not exposed on the type interface.
|
||||
*/
|
||||
this.handleValueChange(el, el.checked);
|
||||
}
|
||||
}
|
||||
|
||||
export declare interface IonRadio extends Components.IonRadio {
|
||||
/**
|
||||
* Emitted when the radio button has focus.
|
||||
*/
|
||||
ionFocus: EventEmitter<CustomEvent<void>>;
|
||||
/**
|
||||
* Emitted when the radio button loses focus.
|
||||
*/
|
||||
ionBlur: EventEmitter<CustomEvent<void>>;
|
||||
}
|
||||
112
packages/angular/standalone/src/directives/range.ts
Normal file
112
packages/angular/standalone/src/directives/range.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Injector,
|
||||
NgZone,
|
||||
} from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { ValueAccessor } from '@ionic/angular/common';
|
||||
import type {
|
||||
RangeChangeEventDetail,
|
||||
RangeKnobMoveStartEventDetail,
|
||||
RangeKnobMoveEndEventDetail,
|
||||
Components,
|
||||
} from '@ionic/core/components';
|
||||
import { defineCustomElement } from '@ionic/core/components/ion-range.js';
|
||||
|
||||
import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';
|
||||
|
||||
const RANGE_INPUTS = [
|
||||
'activeBarStart',
|
||||
'color',
|
||||
'debounce',
|
||||
'disabled',
|
||||
'dualKnobs',
|
||||
'label',
|
||||
'labelPlacement',
|
||||
'legacy',
|
||||
'max',
|
||||
'min',
|
||||
'mode',
|
||||
'name',
|
||||
'pin',
|
||||
'pinFormatter',
|
||||
'snaps',
|
||||
'step',
|
||||
'ticks',
|
||||
'value',
|
||||
];
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineCustomElement,
|
||||
inputs: RANGE_INPUTS,
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-range',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: RANGE_INPUTS,
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: IonRange,
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
export class IonRange extends ValueAccessor {
|
||||
protected el: HTMLElement;
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) {
|
||||
super(injector, r);
|
||||
c.detach();
|
||||
this.el = r.nativeElement;
|
||||
proxyOutputs(this, this.el, ['ionChange', 'ionInput', 'ionFocus', 'ionBlur', 'ionKnobMoveStart', 'ionKnobMoveEnd']);
|
||||
}
|
||||
|
||||
@HostListener('ionChange', ['$event.target'])
|
||||
handleIonChange(el: HTMLIonRangeElement): void {
|
||||
this.handleValueChange(el, el.value);
|
||||
}
|
||||
}
|
||||
|
||||
export declare interface IonRange extends Components.IonRange {
|
||||
/**
|
||||
* The `ionChange` event is fired for `<ion-range>` elements when the user
|
||||
modifies the element's value:
|
||||
- When the user releases the knob after dragging;
|
||||
- When the user moves the knob with keyboard arrows
|
||||
|
||||
`ionChange` is not fired when the value is changed programmatically.
|
||||
*/
|
||||
ionChange: EventEmitter<CustomEvent<RangeChangeEventDetail>>;
|
||||
/**
|
||||
* The `ionInput` event is fired for `<ion-range>` elements when the value
|
||||
is modified. Unlike `ionChange`, `ionInput` is fired continuously
|
||||
while the user is dragging the knob.
|
||||
*/
|
||||
ionInput: EventEmitter<CustomEvent<RangeChangeEventDetail>>;
|
||||
/**
|
||||
* Emitted when the range has focus.
|
||||
*/
|
||||
ionFocus: EventEmitter<CustomEvent<void>>;
|
||||
/**
|
||||
* Emitted when the range loses focus.
|
||||
*/
|
||||
ionBlur: EventEmitter<CustomEvent<void>>;
|
||||
/**
|
||||
* Emitted when the user starts moving the range knob, whether through
|
||||
mouse drag, touch gesture, or keyboard interaction.
|
||||
*/
|
||||
ionKnobMoveStart: EventEmitter<CustomEvent<RangeKnobMoveStartEventDetail>>;
|
||||
/**
|
||||
* Emitted when the user finishes moving the range knob, whether through
|
||||
mouse drag, touch gesture, or keyboard interaction.
|
||||
*/
|
||||
ionKnobMoveEnd: EventEmitter<CustomEvent<RangeKnobMoveEndEventDetail>>;
|
||||
}
|
||||
108
packages/angular/standalone/src/directives/searchbar.ts
Normal file
108
packages/angular/standalone/src/directives/searchbar.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Injector,
|
||||
NgZone,
|
||||
} from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { ValueAccessor } from '@ionic/angular/common';
|
||||
import type { SearchbarInputEventDetail, SearchbarChangeEventDetail, Components } from '@ionic/core/components';
|
||||
import { defineCustomElement } from '@ionic/core/components/ion-searchbar.js';
|
||||
|
||||
import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';
|
||||
|
||||
const SEARCHBAR_INPUTS = [
|
||||
'animated',
|
||||
'autocomplete',
|
||||
'autocorrect',
|
||||
'cancelButtonIcon',
|
||||
'cancelButtonText',
|
||||
'clearIcon',
|
||||
'color',
|
||||
'debounce',
|
||||
'disabled',
|
||||
'enterkeyhint',
|
||||
'inputmode',
|
||||
'mode',
|
||||
'name',
|
||||
'placeholder',
|
||||
'searchIcon',
|
||||
'showCancelButton',
|
||||
'showClearButton',
|
||||
'spellcheck',
|
||||
'type',
|
||||
'value',
|
||||
];
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineCustomElement,
|
||||
inputs: SEARCHBAR_INPUTS,
|
||||
methods: ['setFocus', 'getInputElement'],
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-searchbar',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: SEARCHBAR_INPUTS,
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: IonSearchbar,
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
export class IonSearchbar extends ValueAccessor {
|
||||
protected el: HTMLElement;
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) {
|
||||
super(injector, r);
|
||||
c.detach();
|
||||
this.el = r.nativeElement;
|
||||
proxyOutputs(this, this.el, ['ionInput', 'ionChange', 'ionCancel', 'ionClear', 'ionBlur', 'ionFocus']);
|
||||
}
|
||||
|
||||
@HostListener('ionInput', ['$event.target'])
|
||||
handleIonInput(el: HTMLIonSearchbarElement): void {
|
||||
this.handleValueChange(el, el.value);
|
||||
}
|
||||
}
|
||||
|
||||
export declare interface IonSearchbar extends Components.IonSearchbar {
|
||||
/**
|
||||
* Emitted when the `value` of the `ion-searchbar` element has changed.
|
||||
*/
|
||||
ionInput: EventEmitter<CustomEvent<SearchbarInputEventDetail>>;
|
||||
/**
|
||||
* The `ionChange` event is fired for `<ion-searchbar>` elements when the user
|
||||
modifies the element's value. Unlike the `ionInput` event, the `ionChange`
|
||||
event is not necessarily fired for each alteration to an element's value.
|
||||
|
||||
The `ionChange` event is fired when the value has been committed
|
||||
by the user. This can happen when the element loses focus or
|
||||
when the "Enter" key is pressed. `ionChange` can also fire
|
||||
when clicking the clear or cancel buttons.
|
||||
*/
|
||||
ionChange: EventEmitter<CustomEvent<SearchbarChangeEventDetail>>;
|
||||
/**
|
||||
* Emitted when the cancel button is clicked.
|
||||
*/
|
||||
ionCancel: EventEmitter<CustomEvent<void>>;
|
||||
/**
|
||||
* Emitted when the clear input button is clicked.
|
||||
*/
|
||||
ionClear: EventEmitter<CustomEvent<void>>;
|
||||
/**
|
||||
* Emitted when the input loses focus.
|
||||
*/
|
||||
ionBlur: EventEmitter<CustomEvent<void>>;
|
||||
/**
|
||||
* Emitted when the input has focus.
|
||||
*/
|
||||
ionFocus: EventEmitter<CustomEvent<void>>;
|
||||
}
|
||||
60
packages/angular/standalone/src/directives/segment.ts
Normal file
60
packages/angular/standalone/src/directives/segment.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Injector,
|
||||
NgZone,
|
||||
} from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { ValueAccessor } from '@ionic/angular/common';
|
||||
import type { SegmentChangeEventDetail, Components } from '@ionic/core/components';
|
||||
import { defineCustomElement } from '@ionic/core/components/ion-segment.js';
|
||||
|
||||
import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';
|
||||
|
||||
const SEGMENT_INPUTS = ['color', 'disabled', 'mode', 'scrollable', 'selectOnFocus', 'swipeGesture', 'value'];
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineCustomElement,
|
||||
inputs: SEGMENT_INPUTS,
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-segment',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: SEGMENT_INPUTS,
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: IonSegment,
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
export class IonSegment extends ValueAccessor {
|
||||
protected el: HTMLElement;
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) {
|
||||
super(injector, r);
|
||||
c.detach();
|
||||
this.el = r.nativeElement;
|
||||
proxyOutputs(this, this.el, ['ionChange']);
|
||||
}
|
||||
|
||||
@HostListener('ionChange', ['$event.target'])
|
||||
handleIonChange(el: HTMLIonSegmentElement): void {
|
||||
this.handleValueChange(el, el.value);
|
||||
}
|
||||
}
|
||||
|
||||
export declare interface IonSegment extends Components.IonSegment {
|
||||
/**
|
||||
* Emitted when the value property has changed and any
|
||||
dragging pointer has been released from `ion-segment`.
|
||||
*/
|
||||
ionChange: EventEmitter<CustomEvent<SegmentChangeEventDetail>>;
|
||||
}
|
||||
98
packages/angular/standalone/src/directives/select.ts
Normal file
98
packages/angular/standalone/src/directives/select.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Injector,
|
||||
NgZone,
|
||||
} from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { ValueAccessor } from '@ionic/angular/common';
|
||||
import type { SelectChangeEventDetail, Components } from '@ionic/core/components';
|
||||
import { defineCustomElement } from '@ionic/core/components/ion-select.js';
|
||||
|
||||
import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';
|
||||
|
||||
const SELECT_INPUTS = [
|
||||
'cancelText',
|
||||
'color',
|
||||
'compareWith',
|
||||
'disabled',
|
||||
'expandedIcon',
|
||||
'fill',
|
||||
'interface',
|
||||
'interfaceOptions',
|
||||
'justify',
|
||||
'label',
|
||||
'labelPlacement',
|
||||
'legacy',
|
||||
'mode',
|
||||
'multiple',
|
||||
'name',
|
||||
'okText',
|
||||
'placeholder',
|
||||
'selectedText',
|
||||
'shape',
|
||||
'toggleIcon',
|
||||
'value',
|
||||
];
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineCustomElement,
|
||||
inputs: SELECT_INPUTS,
|
||||
methods: ['open'],
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-select',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: SELECT_INPUTS,
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: IonSelect,
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
export class IonSelect extends ValueAccessor {
|
||||
protected el: HTMLElement;
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) {
|
||||
super(injector, r);
|
||||
c.detach();
|
||||
this.el = r.nativeElement;
|
||||
proxyOutputs(this, this.el, ['ionChange', 'ionCancel', 'ionDismiss', 'ionFocus', 'ionBlur']);
|
||||
}
|
||||
|
||||
@HostListener('ionChange', ['$event.target'])
|
||||
handleIonChange(el: HTMLIonSelectElement): void {
|
||||
this.handleValueChange(el, el.value);
|
||||
}
|
||||
}
|
||||
|
||||
export declare interface IonSelect extends Components.IonSelect {
|
||||
/**
|
||||
* Emitted when the value has changed.
|
||||
*/
|
||||
ionChange: EventEmitter<CustomEvent<SelectChangeEventDetail>>;
|
||||
/**
|
||||
* Emitted when the selection is cancelled.
|
||||
*/
|
||||
ionCancel: EventEmitter<CustomEvent<void>>;
|
||||
/**
|
||||
* Emitted when the overlay is dismissed.
|
||||
*/
|
||||
ionDismiss: EventEmitter<CustomEvent<void>>;
|
||||
/**
|
||||
* Emitted when the select has focus.
|
||||
*/
|
||||
ionFocus: EventEmitter<CustomEvent<void>>;
|
||||
/**
|
||||
* Emitted when the select loses focus.
|
||||
*/
|
||||
ionBlur: EventEmitter<CustomEvent<void>>;
|
||||
}
|
||||
110
packages/angular/standalone/src/directives/textarea.ts
Normal file
110
packages/angular/standalone/src/directives/textarea.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Injector,
|
||||
NgZone,
|
||||
} from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { ValueAccessor } from '@ionic/angular/common';
|
||||
import type { TextareaChangeEventDetail, TextareaInputEventDetail, Components } from '@ionic/core/components';
|
||||
import { defineCustomElement } from '@ionic/core/components/ion-textarea.js';
|
||||
|
||||
import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';
|
||||
|
||||
const TEXTAREA_INPUTS = [
|
||||
'autoGrow',
|
||||
'autocapitalize',
|
||||
'autofocus',
|
||||
'clearOnEdit',
|
||||
'color',
|
||||
'cols',
|
||||
'counter',
|
||||
'counterFormatter',
|
||||
'debounce',
|
||||
'disabled',
|
||||
'enterkeyhint',
|
||||
'errorText',
|
||||
'fill',
|
||||
'helperText',
|
||||
'inputmode',
|
||||
'label',
|
||||
'labelPlacement',
|
||||
'legacy',
|
||||
'maxlength',
|
||||
'minlength',
|
||||
'mode',
|
||||
'name',
|
||||
'placeholder',
|
||||
'readonly',
|
||||
'required',
|
||||
'rows',
|
||||
'shape',
|
||||
'spellcheck',
|
||||
'value',
|
||||
'wrap',
|
||||
];
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineCustomElement,
|
||||
inputs: TEXTAREA_INPUTS,
|
||||
methods: ['setFocus', 'getInputElement'],
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-textarea',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: TEXTAREA_INPUTS,
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: IonTextarea,
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
export class IonTextarea extends ValueAccessor {
|
||||
protected el: HTMLElement;
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) {
|
||||
super(injector, r);
|
||||
c.detach();
|
||||
this.el = r.nativeElement;
|
||||
proxyOutputs(this, this.el, ['ionChange', 'ionInput', 'ionBlur', 'ionFocus']);
|
||||
}
|
||||
|
||||
@HostListener('ionInput', ['$event.target'])
|
||||
handleIonInput(el: HTMLIonTextareaElement): void {
|
||||
this.handleValueChange(el, el.value);
|
||||
}
|
||||
}
|
||||
|
||||
export declare interface IonTextarea extends Components.IonTextarea {
|
||||
/**
|
||||
* The `ionChange` event is fired when the user modifies the textarea's value.
|
||||
Unlike the `ionInput` event, the `ionChange` event is fired when
|
||||
the element loses focus after its value has been modified.
|
||||
*/
|
||||
ionChange: EventEmitter<CustomEvent<TextareaChangeEventDetail>>;
|
||||
/**
|
||||
* The `ionInput` event is fired each time the user modifies the textarea's value.
|
||||
Unlike the `ionChange` event, the `ionInput` event is fired for each alteration
|
||||
to the textarea's value. This typically happens for each keystroke as the user types.
|
||||
|
||||
When `clearOnEdit` is enabled, the `ionInput` event will be fired when
|
||||
the user clears the textarea by performing a keydown event.
|
||||
*/
|
||||
ionInput: EventEmitter<CustomEvent<TextareaInputEventDetail>>;
|
||||
/**
|
||||
* Emitted when the input loses focus.
|
||||
*/
|
||||
ionBlur: EventEmitter<CustomEvent<FocusEvent>>;
|
||||
/**
|
||||
* Emitted when the input has focus.
|
||||
*/
|
||||
ionFocus: EventEmitter<CustomEvent<FocusEvent>>;
|
||||
}
|
||||
84
packages/angular/standalone/src/directives/toggle.ts
Normal file
84
packages/angular/standalone/src/directives/toggle.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Injector,
|
||||
NgZone,
|
||||
} from '@angular/core';
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { ValueAccessor, setIonicClasses } from '@ionic/angular/common';
|
||||
import type { ToggleChangeEventDetail, Components } from '@ionic/core/components';
|
||||
import { defineCustomElement } from '@ionic/core/components/ion-toggle.js';
|
||||
|
||||
import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils';
|
||||
|
||||
const TOGGLE_INPUTS = [
|
||||
'checked',
|
||||
'color',
|
||||
'disabled',
|
||||
'enableOnOffLabels',
|
||||
'justify',
|
||||
'labelPlacement',
|
||||
'legacy',
|
||||
'mode',
|
||||
'name',
|
||||
'value',
|
||||
];
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineCustomElement,
|
||||
inputs: TOGGLE_INPUTS,
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-toggle',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: TOGGLE_INPUTS,
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: IonToggle,
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
export class IonToggle extends ValueAccessor {
|
||||
protected el: HTMLElement;
|
||||
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) {
|
||||
super(injector, r);
|
||||
c.detach();
|
||||
this.el = r.nativeElement;
|
||||
proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur']);
|
||||
}
|
||||
|
||||
writeValue(value: boolean): void {
|
||||
this.elementRef.nativeElement.checked = this.lastValue = value;
|
||||
setIonicClasses(this.elementRef);
|
||||
}
|
||||
|
||||
@HostListener('ionChange', ['$event.target'])
|
||||
handleIonChange(el: HTMLIonToggleElement): void {
|
||||
this.handleValueChange(el, el.checked);
|
||||
}
|
||||
}
|
||||
|
||||
export declare interface IonToggle extends Components.IonToggle {
|
||||
/**
|
||||
* Emitted when the user switches the toggle on or off. Does not emit
|
||||
when programmatically changing the value of the `checked` property.
|
||||
*/
|
||||
ionChange: EventEmitter<CustomEvent<ToggleChangeEventDetail>>;
|
||||
/**
|
||||
* Emitted when the toggle has focus.
|
||||
*/
|
||||
ionFocus: EventEmitter<CustomEvent<void>>;
|
||||
/**
|
||||
* Emitted when the toggle loses focus.
|
||||
*/
|
||||
ionBlur: EventEmitter<CustomEvent<void>>;
|
||||
}
|
||||
127
packages/angular/standalone/src/index.ts
Normal file
127
packages/angular/standalone/src/index.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
export { IonBackButton } from './navigation/back-button';
|
||||
export { IonModal } from './overlays/modal';
|
||||
export { IonPopover } from './overlays/popover';
|
||||
export { IonRouterOutlet } from './navigation/router-outlet';
|
||||
export { IonRouterLink, IonRouterLinkWithHref } from './navigation/router-link-delegate';
|
||||
export { IonTabs } from './navigation/tabs';
|
||||
export { provideIonicAngular } from './providers/ionic-angular';
|
||||
export {
|
||||
ActionSheetController,
|
||||
AlertController,
|
||||
LoadingController,
|
||||
MenuController,
|
||||
ModalController,
|
||||
PickerController,
|
||||
PopoverController,
|
||||
ToastController,
|
||||
AnimationController,
|
||||
GestureController,
|
||||
DomController,
|
||||
NavController,
|
||||
Config,
|
||||
Platform,
|
||||
NavParams,
|
||||
IonicRouteStrategy,
|
||||
} from '@ionic/angular/common';
|
||||
export { IonNav } from './navigation/nav';
|
||||
export {
|
||||
IonCheckbox,
|
||||
IonDatetime,
|
||||
IonInput,
|
||||
IonIcon,
|
||||
IonRadioGroup,
|
||||
IonRadio,
|
||||
IonRange,
|
||||
IonSearchbar,
|
||||
IonSegment,
|
||||
IonSelect,
|
||||
IonTextarea,
|
||||
IonToggle,
|
||||
} from './directives';
|
||||
export * from './directives/proxies';
|
||||
|
||||
export {
|
||||
// UTILS
|
||||
createAnimation,
|
||||
createGesture,
|
||||
iosTransitionAnimation,
|
||||
mdTransitionAnimation,
|
||||
IonicSlides,
|
||||
getPlatforms,
|
||||
isPlatform,
|
||||
getTimeGivenProgression,
|
||||
// TYPES
|
||||
Animation,
|
||||
AnimationBuilder,
|
||||
AnimationCallbackOptions,
|
||||
AnimationDirection,
|
||||
AnimationFill,
|
||||
AnimationKeyFrames,
|
||||
AnimationLifecycle,
|
||||
Gesture,
|
||||
GestureConfig,
|
||||
GestureDetail,
|
||||
NavComponentWithProps,
|
||||
SpinnerTypes,
|
||||
AccordionGroupCustomEvent,
|
||||
AccordionGroupChangeEventDetail,
|
||||
BreadcrumbCustomEvent,
|
||||
BreadcrumbCollapsedClickEventDetail,
|
||||
ActionSheetOptions,
|
||||
ActionSheetButton,
|
||||
AlertOptions,
|
||||
AlertInput,
|
||||
AlertButton,
|
||||
BackButtonEvent,
|
||||
CheckboxCustomEvent,
|
||||
CheckboxChangeEventDetail,
|
||||
DatetimeCustomEvent,
|
||||
DatetimeChangeEventDetail,
|
||||
InfiniteScrollCustomEvent,
|
||||
InputCustomEvent,
|
||||
InputChangeEventDetail,
|
||||
ItemReorderEventDetail,
|
||||
ItemReorderCustomEvent,
|
||||
ItemSlidingCustomEvent,
|
||||
IonicSafeString,
|
||||
LoadingOptions,
|
||||
MenuCustomEvent,
|
||||
ModalOptions,
|
||||
NavCustomEvent,
|
||||
PickerOptions,
|
||||
PickerButton,
|
||||
PickerColumn,
|
||||
PickerColumnOption,
|
||||
PlatformConfig,
|
||||
PopoverOptions,
|
||||
RadioGroupCustomEvent,
|
||||
RadioGroupChangeEventDetail,
|
||||
RangeCustomEvent,
|
||||
RangeChangeEventDetail,
|
||||
RangeKnobMoveStartEventDetail,
|
||||
RangeKnobMoveEndEventDetail,
|
||||
RefresherCustomEvent,
|
||||
RefresherEventDetail,
|
||||
RouterEventDetail,
|
||||
RouterCustomEvent,
|
||||
ScrollBaseCustomEvent,
|
||||
ScrollBaseDetail,
|
||||
ScrollDetail,
|
||||
ScrollCustomEvent,
|
||||
SearchbarCustomEvent,
|
||||
SearchbarChangeEventDetail,
|
||||
SearchbarInputEventDetail,
|
||||
SegmentChangeEventDetail,
|
||||
SegmentCustomEvent,
|
||||
SegmentValue,
|
||||
SelectChangeEventDetail,
|
||||
SelectCustomEvent,
|
||||
TabsCustomEvent,
|
||||
TextareaChangeEventDetail,
|
||||
TextareaCustomEvent,
|
||||
ToastOptions,
|
||||
ToastButton,
|
||||
ToastLayout,
|
||||
ToggleChangeEventDetail,
|
||||
ToggleCustomEvent,
|
||||
} from '@ionic/core/components';
|
||||
28
packages/angular/standalone/src/navigation/back-button.ts
Normal file
28
packages/angular/standalone/src/navigation/back-button.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Component, Optional, ChangeDetectionStrategy, ElementRef, NgZone, ChangeDetectorRef } from '@angular/core';
|
||||
import { IonBackButton as IonBackButtonBase, NavController, Config, ProxyCmp } from '@ionic/angular/common';
|
||||
import { defineCustomElement } from '@ionic/core/components/ion-back-button.js';
|
||||
|
||||
import { IonRouterOutlet } from './router-outlet';
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineCustomElement,
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-back-button',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: '<ng-content></ng-content>',
|
||||
standalone: true,
|
||||
})
|
||||
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
||||
export class IonBackButton extends IonBackButtonBase {
|
||||
constructor(
|
||||
@Optional() routerOutlet: IonRouterOutlet,
|
||||
navCtrl: NavController,
|
||||
config: Config,
|
||||
r: ElementRef,
|
||||
z: NgZone,
|
||||
c: ChangeDetectorRef
|
||||
) {
|
||||
super(routerOutlet, navCtrl, config, r, z, c);
|
||||
}
|
||||
}
|
||||
24
packages/angular/standalone/src/navigation/nav.ts
Normal file
24
packages/angular/standalone/src/navigation/nav.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Component, ElementRef, Injector, EnvironmentInjector, NgZone, ChangeDetectorRef } from '@angular/core';
|
||||
import { IonNav as IonNavBase, ProxyCmp, AngularDelegate } from '@ionic/angular/common';
|
||||
import { defineCustomElement } from '@ionic/core/components/ion-nav.js';
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineCustomElement,
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-nav',
|
||||
template: '<ng-content></ng-content>',
|
||||
standalone: true,
|
||||
})
|
||||
export class IonNav extends IonNavBase {
|
||||
constructor(
|
||||
ref: ElementRef,
|
||||
environmentInjector: EnvironmentInjector,
|
||||
injector: Injector,
|
||||
angularDelegate: AngularDelegate,
|
||||
z: NgZone,
|
||||
c: ChangeDetectorRef
|
||||
) {
|
||||
super(ref, environmentInjector, injector, angularDelegate, z, c);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Directive } from '@angular/core';
|
||||
import {
|
||||
RouterLinkDelegateDirective as RouterLinkDelegateBase,
|
||||
RouterLinkWithHrefDelegateDirective as RouterLinkHrefDelegateBase,
|
||||
} from '@ionic/angular/common';
|
||||
|
||||
@Directive({
|
||||
selector: ':not(a):not(area)[routerLink]',
|
||||
standalone: true,
|
||||
})
|
||||
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
||||
export class IonRouterLink extends RouterLinkDelegateBase {}
|
||||
|
||||
@Directive({
|
||||
selector: 'a[routerLink],area[routerLink]',
|
||||
standalone: true,
|
||||
})
|
||||
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
||||
export class IonRouterLinkWithHref extends RouterLinkHrefDelegateBase {}
|
||||
13
packages/angular/standalone/src/navigation/router-outlet.ts
Normal file
13
packages/angular/standalone/src/navigation/router-outlet.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Directive } from '@angular/core';
|
||||
import { IonRouterOutlet as IonRouterOutletBase, ProxyCmp } from '@ionic/angular/common';
|
||||
import { defineCustomElement } from '@ionic/core/components/ion-router-outlet.js';
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineCustomElement,
|
||||
})
|
||||
@Directive({
|
||||
selector: 'ion-router-outlet',
|
||||
standalone: true,
|
||||
})
|
||||
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
||||
export class IonRouterOutlet extends IonRouterOutletBase {}
|
||||
57
packages/angular/standalone/src/navigation/tabs.ts
Normal file
57
packages/angular/standalone/src/navigation/tabs.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Component, ContentChild, ContentChildren, ViewChild, QueryList } from '@angular/core';
|
||||
import { IonTabs as IonTabsBase } from '@ionic/angular/common';
|
||||
|
||||
import { IonTabBar } from '../directives/proxies';
|
||||
|
||||
import { IonRouterOutlet } from './router-outlet';
|
||||
|
||||
@Component({
|
||||
selector: 'ion-tabs',
|
||||
template: `
|
||||
<ng-content select="[slot=top]"></ng-content>
|
||||
<div class="tabs-inner" #tabsInner>
|
||||
<ion-router-outlet
|
||||
#outlet
|
||||
tabs="true"
|
||||
(stackWillChange)="onStackWillChange($event)"
|
||||
(stackDidChange)="onStackDidChange($event)"
|
||||
></ion-router-outlet>
|
||||
</div>
|
||||
<ng-content></ng-content>
|
||||
`,
|
||||
standalone: true,
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
contain: layout size style;
|
||||
}
|
||||
.tabs-inner {
|
||||
position: relative;
|
||||
|
||||
flex: 1;
|
||||
|
||||
contain: layout size style;
|
||||
}
|
||||
`,
|
||||
],
|
||||
imports: [IonRouterOutlet],
|
||||
})
|
||||
// eslint-disable-next-line @angular-eslint/component-class-suffix
|
||||
export class IonTabs extends IonTabsBase {
|
||||
@ViewChild('outlet', { read: IonRouterOutlet, static: false }) outlet: IonRouterOutlet;
|
||||
|
||||
@ContentChild(IonTabBar, { static: false }) tabBar: IonTabBar | undefined;
|
||||
@ContentChildren(IonTabBar) tabBars: QueryList<IonTabBar>;
|
||||
}
|
||||
18
packages/angular/standalone/src/overlays/modal.ts
Normal file
18
packages/angular/standalone/src/overlays/modal.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { IonModal as IonModalBase, ProxyCmp } from '@ionic/angular/common';
|
||||
import { defineCustomElement } from '@ionic/core/components/ion-modal.js';
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineCustomElement,
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-modal',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `<div class="ion-delegate-host ion-page" *ngIf="isCmpOpen || keepContentsMounted">
|
||||
<ng-container [ngTemplateOutlet]="template"></ng-container>
|
||||
</div>`,
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
})
|
||||
export class IonModal extends IonModalBase {}
|
||||
16
packages/angular/standalone/src/overlays/popover.ts
Normal file
16
packages/angular/standalone/src/overlays/popover.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
||||
import { IonPopover as IonPopoverBase, ProxyCmp } from '@ionic/angular/common';
|
||||
import { defineCustomElement } from '@ionic/core/components/ion-popover.js';
|
||||
|
||||
@ProxyCmp({
|
||||
defineCustomElementFn: defineCustomElement,
|
||||
})
|
||||
@Component({
|
||||
selector: 'ion-popover',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `<ng-container [ngTemplateOutlet]="template" *ngIf="isCmpOpen || keepContentsMounted"></ng-container>`,
|
||||
standalone: true,
|
||||
imports: [CommonModule],
|
||||
})
|
||||
export class IonPopover extends IonPopoverBase {}
|
||||
51
packages/angular/standalone/src/providers/ionic-angular.ts
Normal file
51
packages/angular/standalone/src/providers/ionic-angular.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { APP_INITIALIZER } from '@angular/core';
|
||||
import type { Provider } from '@angular/core';
|
||||
import {
|
||||
AngularDelegate,
|
||||
ConfigToken,
|
||||
ModalController,
|
||||
PopoverController,
|
||||
provideComponentInputBinding,
|
||||
} from '@ionic/angular/common';
|
||||
import { initialize } from '@ionic/core/components';
|
||||
import type { IonicConfig } from '@ionic/core/components';
|
||||
|
||||
export const provideIonicAngular = (config?: IonicConfig): Provider[] => {
|
||||
/**
|
||||
* TODO FW-4967
|
||||
* Use makeEnvironmentProviders once Angular 14 support is dropped.
|
||||
* This prevents provideIonicAngular from being accidentally referenced in an @Component.
|
||||
*/
|
||||
return [
|
||||
{
|
||||
provide: ConfigToken,
|
||||
useValue: config,
|
||||
},
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: initializeIonicAngular,
|
||||
multi: true,
|
||||
deps: [ConfigToken, DOCUMENT],
|
||||
},
|
||||
provideComponentInputBinding(),
|
||||
AngularDelegate,
|
||||
ModalController,
|
||||
PopoverController,
|
||||
];
|
||||
};
|
||||
|
||||
const initializeIonicAngular = (config: IonicConfig, doc: Document) => {
|
||||
return () => {
|
||||
/**
|
||||
* By default Ionic Framework hides elements that
|
||||
* are not hydrated, but in the CE build there is no
|
||||
* hydration.
|
||||
* TODO FW-2797: Remove when all integrations have been
|
||||
* migrated to CE build.
|
||||
*/
|
||||
doc.documentElement.classList.add('ion-ce');
|
||||
|
||||
initialize(config);
|
||||
};
|
||||
};
|
||||
@@ -70,6 +70,14 @@ If you want to add a version-specific change, add the change inside of the appro
|
||||
|
||||
If you need to add E2E tests that are only run on a specific version of the JS Framework, replicate the `VersionTest` component on each partial application. This ensures that tests for framework version X do not get run for framework version Y.
|
||||
|
||||
### Testing Lazy Loaded Ionic Components
|
||||
|
||||
Tests for lazy loaded Ionic UI components should only be added under the `/lazy` route. This ensures the `IonicModule` is added.
|
||||
|
||||
### Testing Standalone Ionic Components
|
||||
|
||||
Tests for standalone Ionic UI components should only be added under the `/standalone` route. This allows for an isolated environment where the lazy loaded `IonicModule` is not initialized. The standalone components use Stencil's custom element bundle instead of the lazy loaded bundle. If `IonicModule` is initialized then the Stencil components will fall back to using the lazy loaded implementation instead of the custom elements bundle implementation.
|
||||
|
||||
## Adding New Test Apps
|
||||
|
||||
As we add support for new versions of Angular, we will also need to update this directory to test against new applications. The following steps can serve as a guide for adding new apps:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
it("should be on Angular 14", () => {
|
||||
cy.visit('/');
|
||||
cy.visit('/lazy');
|
||||
|
||||
cy.get('ion-title').contains('Angular 14');
|
||||
});
|
||||
55
packages/angular/test/apps/ng14/package-lock.json
generated
55
packages/angular/test/apps/ng14/package-lock.json
generated
@@ -22,7 +22,7 @@
|
||||
"@ionic/angular-server": "^6.1.15",
|
||||
"core-js": "^2.6.11",
|
||||
"express": "^4.15.2",
|
||||
"ionicons": "^6.0.4",
|
||||
"ionicons": "^7.2.0",
|
||||
"rxjs": "~7.5.0",
|
||||
"tslib": "^2.3.0",
|
||||
"typescript-eslint-language-service": "^4.1.5",
|
||||
@@ -2937,6 +2937,14 @@
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ionic/core/node_modules/ionicons": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.1.3.tgz",
|
||||
"integrity": "sha512-ptzz38dd/Yq+PgjhXegh7yhb/SLIk1bvL9vQDtLv1aoSc7alO6mX2DIMgcKYzt9vrNWkRu1f9Jr78zIFFyOXqw==",
|
||||
"dependencies": {
|
||||
"@stencil/core": "^2.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/load-nyc-config": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||
@@ -9955,11 +9963,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ionicons": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.0.4.tgz",
|
||||
"integrity": "sha512-uDNOkBo0OVYV+kIhb51g9mb7r3Z0b+78GPZQBsjXuaetNmrB/mNTqN/uFtO+vxL/rQySKjzk8qeKJI5NWL9Ueg==",
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.2.0.tgz",
|
||||
"integrity": "sha512-gmG6A50iem/wR10K+rJeiYlBu8SjG1lxZ6brPYEI2WYjEw8s6T+bj///C6yjRIDtj+qIgqrQmQAgNvNdgyG7ng==",
|
||||
"dependencies": {
|
||||
"@stencil/core": "^2.18.0"
|
||||
"@stencil/core": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/ionicons/node_modules/@stencil/core": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.4.1.tgz",
|
||||
"integrity": "sha512-SirGcrb5yKHCn2BwdM7HGVXuvCdmwiXlVczEj8jJxQIm42CAUQCUECxtZidTzp+oZBZnWLnoAvfanchJsgkQzA==",
|
||||
"bin": {
|
||||
"stencil": "bin/stencil"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
"npm": ">=7.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ip": {
|
||||
@@ -18576,6 +18596,16 @@
|
||||
"@stencil/core": "^2.16.0",
|
||||
"ionicons": "^6.0.2",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ionicons": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.1.3.tgz",
|
||||
"integrity": "sha512-ptzz38dd/Yq+PgjhXegh7yhb/SLIk1bvL9vQDtLv1aoSc7alO6mX2DIMgcKYzt9vrNWkRu1f9Jr78zIFFyOXqw==",
|
||||
"requires": {
|
||||
"@stencil/core": "^2.18.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@istanbuljs/load-nyc-config": {
|
||||
@@ -23759,11 +23789,18 @@
|
||||
"dev": true
|
||||
},
|
||||
"ionicons": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.0.4.tgz",
|
||||
"integrity": "sha512-uDNOkBo0OVYV+kIhb51g9mb7r3Z0b+78GPZQBsjXuaetNmrB/mNTqN/uFtO+vxL/rQySKjzk8qeKJI5NWL9Ueg==",
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.2.0.tgz",
|
||||
"integrity": "sha512-gmG6A50iem/wR10K+rJeiYlBu8SjG1lxZ6brPYEI2WYjEw8s6T+bj///C6yjRIDtj+qIgqrQmQAgNvNdgyG7ng==",
|
||||
"requires": {
|
||||
"@stencil/core": "^2.18.0"
|
||||
"@stencil/core": "^4.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stencil/core": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.4.1.tgz",
|
||||
"integrity": "sha512-SirGcrb5yKHCn2BwdM7HGVXuvCdmwiXlVczEj8jJxQIm42CAUQCUECxtZidTzp+oZBZnWLnoAvfanchJsgkQzA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"ip": {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@ionic/angular-server": "^6.1.15",
|
||||
"core-js": "^2.6.11",
|
||||
"express": "^4.15.2",
|
||||
"ionicons": "^6.0.4",
|
||||
"ionicons": "^7.2.0",
|
||||
"rxjs": "~7.5.0",
|
||||
"tslib": "^2.3.0",
|
||||
"typescript-eslint-language-service": "^4.1.5",
|
||||
|
||||
23
packages/angular/test/apps/ng14/src/main-standalone.ts
Normal file
23
packages/angular/test/apps/ng14/src/main-standalone.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { importProvidersFrom } from '@angular/core';
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { RouteReuseStrategy } from '@angular/router';
|
||||
import { provideIonicAngular, IonicRouteStrategy } from '@ionic/angular/standalone';
|
||||
|
||||
import { AppComponentStandalone } from './app/app-standalone.component';
|
||||
import { AppRoutingModule } from './app/app-routing.module';
|
||||
|
||||
import { routes } from './app/app.routes';
|
||||
|
||||
export const bootstrapStandalone = () => {
|
||||
bootstrapApplication(AppComponentStandalone, {
|
||||
providers: [
|
||||
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
|
||||
/**
|
||||
* provideRouter is not available in Angular 14, so
|
||||
* we fallback to using AppRoutingModule
|
||||
*/
|
||||
importProvidersFrom(AppRoutingModule),
|
||||
provideIonicAngular({ keyboardHeight: 12345 })
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
it("should be on Angular 15", () => {
|
||||
cy.visit('/');
|
||||
cy.visit('/lazy');
|
||||
|
||||
cy.get('ion-title').contains('Angular 15');
|
||||
});
|
||||
55
packages/angular/test/apps/ng15/package-lock.json
generated
55
packages/angular/test/apps/ng15/package-lock.json
generated
@@ -23,7 +23,7 @@
|
||||
"@nguniversal/express-engine": "^15.0.0",
|
||||
"core-js": "^2.6.11",
|
||||
"express": "^4.15.2",
|
||||
"ionicons": "^6.0.4",
|
||||
"ionicons": "^7.2.0",
|
||||
"rxjs": "~7.5.0",
|
||||
"tslib": "^2.3.0",
|
||||
"typescript-eslint-language-service": "^4.1.5",
|
||||
@@ -2662,6 +2662,14 @@
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ionic/core/node_modules/ionicons": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.1.3.tgz",
|
||||
"integrity": "sha512-ptzz38dd/Yq+PgjhXegh7yhb/SLIk1bvL9vQDtLv1aoSc7alO6mX2DIMgcKYzt9vrNWkRu1f9Jr78zIFFyOXqw==",
|
||||
"dependencies": {
|
||||
"@stencil/core": "^2.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/load-nyc-config": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||
@@ -9538,11 +9546,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ionicons": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.0.4.tgz",
|
||||
"integrity": "sha512-uDNOkBo0OVYV+kIhb51g9mb7r3Z0b+78GPZQBsjXuaetNmrB/mNTqN/uFtO+vxL/rQySKjzk8qeKJI5NWL9Ueg==",
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.2.0.tgz",
|
||||
"integrity": "sha512-gmG6A50iem/wR10K+rJeiYlBu8SjG1lxZ6brPYEI2WYjEw8s6T+bj///C6yjRIDtj+qIgqrQmQAgNvNdgyG7ng==",
|
||||
"dependencies": {
|
||||
"@stencil/core": "^2.18.0"
|
||||
"@stencil/core": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/ionicons/node_modules/@stencil/core": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.4.1.tgz",
|
||||
"integrity": "sha512-SirGcrb5yKHCn2BwdM7HGVXuvCdmwiXlVczEj8jJxQIm42CAUQCUECxtZidTzp+oZBZnWLnoAvfanchJsgkQzA==",
|
||||
"bin": {
|
||||
"stencil": "bin/stencil"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
"npm": ">=7.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ip": {
|
||||
@@ -17555,6 +17575,16 @@
|
||||
"@stencil/core": "^2.16.0",
|
||||
"ionicons": "^6.0.2",
|
||||
"tslib": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ionicons": {
|
||||
"version": "6.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.1.3.tgz",
|
||||
"integrity": "sha512-ptzz38dd/Yq+PgjhXegh7yhb/SLIk1bvL9vQDtLv1aoSc7alO6mX2DIMgcKYzt9vrNWkRu1f9Jr78zIFFyOXqw==",
|
||||
"requires": {
|
||||
"@stencil/core": "^2.18.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@istanbuljs/load-nyc-config": {
|
||||
@@ -22624,11 +22654,18 @@
|
||||
"dev": true
|
||||
},
|
||||
"ionicons": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.0.4.tgz",
|
||||
"integrity": "sha512-uDNOkBo0OVYV+kIhb51g9mb7r3Z0b+78GPZQBsjXuaetNmrB/mNTqN/uFtO+vxL/rQySKjzk8qeKJI5NWL9Ueg==",
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.2.0.tgz",
|
||||
"integrity": "sha512-gmG6A50iem/wR10K+rJeiYlBu8SjG1lxZ6brPYEI2WYjEw8s6T+bj///C6yjRIDtj+qIgqrQmQAgNvNdgyG7ng==",
|
||||
"requires": {
|
||||
"@stencil/core": "^2.18.0"
|
||||
"@stencil/core": "^4.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stencil/core": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.4.1.tgz",
|
||||
"integrity": "sha512-SirGcrb5yKHCn2BwdM7HGVXuvCdmwiXlVczEj8jJxQIm42CAUQCUECxtZidTzp+oZBZnWLnoAvfanchJsgkQzA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"ip": {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"@nguniversal/express-engine": "^15.0.0",
|
||||
"core-js": "^2.6.11",
|
||||
"express": "^4.15.2",
|
||||
"ionicons": "^6.0.4",
|
||||
"ionicons": "^7.2.0",
|
||||
"rxjs": "~7.5.0",
|
||||
"tslib": "^2.3.0",
|
||||
"typescript-eslint-language-service": "^4.1.5",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
it("should be on Angular 16", () => {
|
||||
cy.visit('/');
|
||||
cy.visit('/lazy');
|
||||
|
||||
cy.get('ion-title').contains('Angular 16');
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
it("binding route data to inputs should work", () => {
|
||||
cy.visit('/version-test/bind-route/test?query=test');
|
||||
cy.visit('/lazy/version-test/bind-route/test?query=test');
|
||||
|
||||
cy.get('#route-params').contains('test');
|
||||
cy.get('#query-params').contains('test');
|
||||
@@ -1,7 +1,7 @@
|
||||
describe('Modal Nav Params', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/version-test/modal-nav-params');
|
||||
cy.visit('/lazy/version-test/modal-nav-params');
|
||||
});
|
||||
|
||||
it('should assign the rootParams when presented in a modal multiple times', () => {
|
||||
34
packages/angular/test/apps/ng16/package-lock.json
generated
34
packages/angular/test/apps/ng16/package-lock.json
generated
@@ -22,7 +22,7 @@
|
||||
"@nguniversal/express-engine": "^16.0.0",
|
||||
"core-js": "^2.6.11",
|
||||
"express": "^4.15.2",
|
||||
"ionicons": "^7.0.4",
|
||||
"ionicons": "^7.2.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"typescript-eslint-language-service": "^4.1.5",
|
||||
@@ -3986,15 +3986,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@stencil/core": {
|
||||
"version": "2.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.19.2.tgz",
|
||||
"integrity": "sha512-TK3sHqyQAACwcac4fWntypFtN9c/y+y9ioZojeCnNDY3dxF33Ax70lL9ZLDyQnWWR+aTh1WFoqB5sYOnZIUKMA==",
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.4.1.tgz",
|
||||
"integrity": "sha512-SirGcrb5yKHCn2BwdM7HGVXuvCdmwiXlVczEj8jJxQIm42CAUQCUECxtZidTzp+oZBZnWLnoAvfanchJsgkQzA==",
|
||||
"bin": {
|
||||
"stencil": "bin/stencil"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.10.0",
|
||||
"npm": ">=6.0.0"
|
||||
"node": ">=16.0.0",
|
||||
"npm": ">=7.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tootallnate/once": {
|
||||
@@ -10126,11 +10126,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ionicons": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.1.0.tgz",
|
||||
"integrity": "sha512-iE4GuEdEHARJpp0sWL7WJZCzNCf5VxpNRhAjW0fLnZPnNL5qZOJUcfup2Z2Ty7Jk8Q5hacrHfGEB1lCwOdXqGg==",
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.2.0.tgz",
|
||||
"integrity": "sha512-gmG6A50iem/wR10K+rJeiYlBu8SjG1lxZ6brPYEI2WYjEw8s6T+bj///C6yjRIDtj+qIgqrQmQAgNvNdgyG7ng==",
|
||||
"dependencies": {
|
||||
"@stencil/core": "^2.18.0"
|
||||
"@stencil/core": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/ip": {
|
||||
@@ -19664,9 +19664,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@stencil/core": {
|
||||
"version": "2.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.19.2.tgz",
|
||||
"integrity": "sha512-TK3sHqyQAACwcac4fWntypFtN9c/y+y9ioZojeCnNDY3dxF33Ax70lL9ZLDyQnWWR+aTh1WFoqB5sYOnZIUKMA=="
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.4.1.tgz",
|
||||
"integrity": "sha512-SirGcrb5yKHCn2BwdM7HGVXuvCdmwiXlVczEj8jJxQIm42CAUQCUECxtZidTzp+oZBZnWLnoAvfanchJsgkQzA=="
|
||||
},
|
||||
"@tootallnate/once": {
|
||||
"version": "2.0.0",
|
||||
@@ -24282,11 +24282,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"ionicons": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.1.0.tgz",
|
||||
"integrity": "sha512-iE4GuEdEHARJpp0sWL7WJZCzNCf5VxpNRhAjW0fLnZPnNL5qZOJUcfup2Z2Ty7Jk8Q5hacrHfGEB1lCwOdXqGg==",
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.2.0.tgz",
|
||||
"integrity": "sha512-gmG6A50iem/wR10K+rJeiYlBu8SjG1lxZ6brPYEI2WYjEw8s6T+bj///C6yjRIDtj+qIgqrQmQAgNvNdgyG7ng==",
|
||||
"requires": {
|
||||
"@stencil/core": "^2.18.0"
|
||||
"@stencil/core": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"ip": {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@nguniversal/express-engine": "^16.0.0",
|
||||
"core-js": "^2.6.11",
|
||||
"express": "^4.15.2",
|
||||
"ionicons": "^7.0.4",
|
||||
"ionicons": "^7.2.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"typescript-eslint-language-service": "^4.1.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
describe('Accordion', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/accordions');
|
||||
cy.visit('/lazy/accordions');
|
||||
});
|
||||
|
||||
it('should correctly expand on multiple modal opens', () => {
|
||||
@@ -1,7 +1,7 @@
|
||||
describe('Form Controls: Range', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit('/form-controls/range');
|
||||
cy.visit('/lazy/form-controls/range');
|
||||
});
|
||||
|
||||
it('should have form control initial value', () => {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user