feature(angular): action-sheet, alert, loading, toast

This commit is contained in:
Dan Bucholtz
2017-12-06 11:48:24 -06:00
committed by GitHub
parent c76b74029a
commit cefbee9ea2
29 changed files with 1302 additions and 3537 deletions

View File

@ -0,0 +1,60 @@
import {
ChangeDetectorRef,
Component,
OnInit,
ReflectiveInjector,
ViewContainerRef,
ViewChild
} from '@angular/core';
import { PublicNav } from '@ionic/core';
import { getProviders } from '../di/di';
import { AngularFrameworkDelegate } from '../providers/angular-framework-delegate';
import { AngularViewController } from '../types/angular-view-controller';
@Component({
selector: 'ion-nav',
template: `
<div #viewport class="ng-nav-viewport"></div>
`
})
export class IonNavDelegate implements OnInit {
@ViewChild('viewport', { read: ViewContainerRef}) viewport: ViewContainerRef;
constructor(private changeDetection: ChangeDetectorRef, private angularFrameworkDelegate: AngularFrameworkDelegate) {
}
ngOnInit() {
const controllerElement = document.querySelector('ion-nav-controller') as any;
controllerElement.delegate = this;
}
attachViewToDom(nav: PublicNav, enteringView: AngularViewController): Promise<any> {
const componentProviders = ReflectiveInjector.resolve(getProviders(nav.element as HTMLIonNavElement));
return this.angularFrameworkDelegate.attachViewToDom(enteringView.component, this.viewport, componentProviders, this.changeDetection).then((angularMountingData) => {
enteringView.componentFactory = angularMountingData.componentFactory;
enteringView.injector = angularMountingData.childInjector;
enteringView.componentRef = angularMountingData.componentRef;
enteringView.instance = angularMountingData.componentRef.instance;
enteringView.angularHostElement = angularMountingData.componentRef.location.nativeElement;
enteringView.element = angularMountingData.componentRef.location.nativeElement.querySelector('ion-page');
});
}
removeViewFromDom(_nav: PublicNav, viewController: AngularViewController) {
return this.angularFrameworkDelegate.removeViewFromDom((viewController as any).componentRef).then(() => {
viewController.componentFactory = null;
viewController.injector = null;
viewController.componentRef = null;
viewController.instance = null;
viewController.angularHostElement = null;
viewController.element = null;
});
}
}

View File

@ -0,0 +1,37 @@
import { InjectionToken } from '@angular/core';
import { App } from '../providers/app';
import { NavController } from '../providers/nav-controller';
export const NavControllerToken = new InjectionToken<any>('NavControllerToken');
export const ViewControllerToken = new InjectionToken<any>('ViewControllerToken');
export const AppToken = new InjectionToken<any>('AppToken');
export function getProviders(element: HTMLIonNavElement) {
return [
{
provide: NavControllerToken, useValue: element
},
{
provide: NavController, useFactory: provideNavControllerInjectable, deps: [NavControllerToken]
},
{
provide: AppToken, useValue: null,
},
{
provide: App, useFactory: provideAppInjectable, deps: [AppToken]
}
]
}
export function provideNavControllerInjectable(element: HTMLIonNavElement) {
return new NavController(element);
}
export function provideAppInjectable() {
const element = document.querySelector('ion-app');
return new App(element);
}

View File

@ -1,110 +0,0 @@
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ComponentFactoryResolver,
Directive,
ElementRef,
Injector,
InjectionToken,
NgZone,
OnInit,
ReflectiveInjector,
ViewContainerRef,
ViewChild
} from '@angular/core';
import { PublicNav, PublicViewController } from '@ionic/core';
import { App } from '../providers/app';
import { NavController as InjectableNavController } from '../providers/nav-controller';
@Component({
selector: 'ion-nav',
template: `
<div #viewport class="ng-nav-viewport"></div>
`
})
export class IonNavDelegate {
@ViewChild('viewport', { read: ViewContainerRef}) viewport: ViewContainerRef;
constructor(private crf: ComponentFactoryResolver, private changeDetection: ChangeDetectorRef, private zone: NgZone, private injector: Injector) {
}
ngOnInit() {
const controllerElement = document.querySelector('ion-nav-controller') as any;
controllerElement.delegate = this;
}
attachViewToDom(nav: PublicNav, enteringView: PublicViewController): Promise<any> {
return new Promise((resolve, reject) => {
this.zone.run(() => {
const componentProviders = ReflectiveInjector.resolve([
{
provide: NavControllerToken, useValue: nav.element,
},
{
provide: InjectableNavController, useFactory: provideNavControllerInjectable, deps: [NavControllerToken]
},
{
provide: AppToken, useValue: null,
},
{
provide: App, useFactory: provideAppInjectable, deps: [AppToken]
}
]);
const componentFactory = this.crf.resolveComponentFactory(enteringView.component);
const childInjector = ReflectiveInjector.fromResolvedProviders(componentProviders, this.viewport.parentInjector);
const componentRef = componentFactory.create(childInjector, []);
this.viewport.insert(componentRef.hostView, this.viewport.length);
this.changeDetection.detectChanges();
(enteringView as any).componentFactory = componentFactory;
(enteringView as any).childInjector = childInjector;
(enteringView as any).componentRef = componentRef;
enteringView.instance = componentRef.instance;
(enteringView as any).angularHostElement = componentRef.location.nativeElement;
enteringView.element = componentRef.location.nativeElement.querySelector('ion-page');
resolve();
});
});
}
removeViewFromDom(nav: PublicNav, viewController: PublicViewController) {
return new Promise((resolve, reject) => {
this.zone.run(() => {
(viewController as any).componentRef.destroy();
// (nav.element as HTMLElement).removeChild(viewController.angularHostElement);
(viewController as any).componentFactory = null;
(viewController as any).childInjector = null;
(viewController as any).componentRef = null;
viewController.instance = null;
(viewController as any).angularHostElement = null;
viewController.element = null;
resolve();
});
})
}
}
export const NavControllerToken = new InjectionToken<any>('NavControllerToken');
export const ViewControllerToken = new InjectionToken<any>('ViewControllerToken');
export const AppToken = new InjectionToken<any>('AppToken');
export function provideNavControllerInjectable(element: any) {
return new InjectableNavController(element);
}
export function provideAppInjectable() {
return new App();
}

View File

@ -1,6 +1,10 @@
export { IonNavDelegate } from './directives/ion-nav';
export { IonNavDelegate } from './components/ion-nav';
export { IonicAngularModule } from './module';
export { ActionSheetController, ActionSheetProxy } from './providers/action-sheet-controller';
export { AlertController, AlertProxy } from './providers/alert-controller';
export { App } from './providers/app';
export { LoadingController, LoadingProxy } from './providers/loading-controller';
export { NavController } from './providers/nav-controller';
export { ToastController, ToastProxy } from './providers/toast-controller';

View File

@ -9,9 +9,15 @@ import { RadioValueAccessor } from './control-value-accessors/radio-value-access
import { SelectValueAccessor } from './control-value-accessors/select-value-accessor';
import { TextValueAccessor } from './control-value-accessors/text-value-accessor';
import { IonNavDelegate } from './directives/ion-nav';
import { IonNavDelegate } from './components/ion-nav';
/* Providers */
import { ActionSheetController } from './providers/action-sheet-controller';
import { AlertController } from './providers/alert-controller';
import { AngularFrameworkDelegate } from './providers/angular-framework-delegate';
import { LoadingController } from './providers/loading-controller';
import { ToastController } from './providers/toast-controller';
@NgModule({
declarations: [
@ -30,14 +36,18 @@ import { AlertController } from './providers/alert-controller';
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA
]
],
})
export class IonicAngularModule {
static forRoot(): ModuleWithProviders {
return {
ngModule: IonicAngularModule,
providers: [
AlertController
AlertController,
ActionSheetController,
AngularFrameworkDelegate,
LoadingController,
ToastController
]
};
}

View File

@ -0,0 +1,104 @@
import { Injectable } from '@angular/core';
import { ActionSheetDismissEvent, ActionSheetOptions } from '@ionic/core';
import { ensureElementInBody, hydrateElement } from '../util/util';
let actionSheetId = 0;
@Injectable()
export class ActionSheetController {
create(opts?: ActionSheetOptions): ActionSheetProxy {
return getActionSheetProxy(opts);
}
}
export function getActionSheetProxy(opts: ActionSheetOptions){
return {
id: actionSheetId++,
state: PRESENTING,
opts: opts,
present: function() { return present(this)},
dismiss: function() { return dismiss(this)},
onDidDismiss: function(callback: (data: any, role: string) => void) {
(this as ActionSheetProxyInternal).onDidDismissHandler = callback;
},
onWillDismiss: function(callback: (data: any, role: string) => void) {
(this as ActionSheetProxyInternal).onWillDismissHandler = callback;
},
}
}
export function present(actionSheetProxy: ActionSheetProxyInternal): Promise<any> {
actionSheetProxy.state = PRESENTING;
return loadOverlay(actionSheetProxy.opts).then((actionSheetElement: HTMLIonActionSheetElement) => {
actionSheetProxy.element = actionSheetElement;
const onDidDismissHandler = (event: ActionSheetDismissEvent) => {
actionSheetElement.removeEventListener(ION_ACTION_SHEET_DID_DISMISS_EVENT, onDidDismissHandler);
if (actionSheetProxy.onDidDismissHandler) {
actionSheetProxy.onDidDismissHandler(event.detail.data, event.detail.role);
}
};
const onWillDismissHandler = (event: ActionSheetDismissEvent) => {
actionSheetElement.removeEventListener(ION_ACTION_SHEET_WILL_DISMISS_EVENT, onWillDismissHandler);
if (actionSheetProxy.onWillDismissHandler) {
actionSheetProxy.onWillDismissHandler(event.detail.data, event.detail.role);
}
};
actionSheetElement.addEventListener(ION_ACTION_SHEET_DID_DISMISS_EVENT, onDidDismissHandler);
actionSheetElement.addEventListener(ION_ACTION_SHEET_WILL_DISMISS_EVENT, onWillDismissHandler);
if (actionSheetProxy.state === PRESENTING) {
return actionSheetElement.present();
}
// we'll only ever get here if someone tried to dismiss the overlay or mess with it's internal state
// attribute before it could async load and present itself.
// with that in mind, just return null to make the TS compiler happy
return null;
});
}
export function dismiss(actionSheetProxy: ActionSheetProxyInternal): Promise<any> {
actionSheetProxy.state = DISMISSING;
if (actionSheetProxy.element) {
if (actionSheetProxy.state === DISMISSING) {
return actionSheetProxy.element.dismiss();
}
}
// either we're not in the dismissing state
// or we're calling this before the element is created
// so just return a resolved promise
return Promise.resolve();
}
export function loadOverlay(opts: ActionSheetOptions): Promise<HTMLIonActionSheetElement> {
const element = ensureElementInBody('ion-action-sheet-controller') as HTMLIonActionSheetControllerElement;
return hydrateElement(element).then(() => {
return element.create(opts);
});
}
export interface ActionSheetProxy {
present(): Promise<void>
dismiss(): Promise<void>
onDidDismiss(callback: (data: any, role: string) => void): void;
onWillDismiss(callback: (data: any, role: string) => void): void;
}
export interface ActionSheetProxyInternal extends ActionSheetProxy {
id: number;
opts: ActionSheetOptions;
state: number;
element: HTMLIonActionSheetElement;
onDidDismissHandler?: (data: any, role: string) => void;
onWillDismissHandler?: (data: any, role: string) => void;
}
export const PRESENTING = 1;
export const DISMISSING = 2;
const ION_ACTION_SHEET_DID_DISMISS_EVENT = 'ionActionSheetDidDismiss';
const ION_ACTION_SHEET_WILL_DISMISS_EVENT = 'ionActionSheetWillDismiss';

View File

@ -53,6 +53,11 @@ export function present(alertProxy: AlertProxyInternal): Promise<any> {
if (alertProxy.state === PRESENTING) {
return alertElement.present();
}
// we'll only ever get here if someone tried to dismiss the overlay or mess with it's internal state
// attribute before it could async load and present itself.
// with that in mind, just return null to make the TS compiler happy
return null;
});
}
@ -69,7 +74,7 @@ export function dismiss(alertProxy: AlertProxyInternal): Promise<any> {
return Promise.resolve();
}
export function loadOverlay(opts: AlertOptions) {
export function loadOverlay(opts: AlertOptions): Promise<HTMLIonAlertElement> {
const element = ensureElementInBody('ion-alert-controller') as HTMLIonAlertControllerElement;
return hydrateElement(element).then(() => {
return element.create(opts);

View File

@ -0,0 +1,60 @@
import {
ChangeDetectorRef,
ComponentFactoryResolver,
ComponentRef,
Injectable,
Injector,
NgZone,
ReflectiveInjector,
Type,
ViewContainerRef,
} from '@angular/core';
import { ComponentFactory } from '@angular/core/src/linker/component_factory';
@Injectable()
export class AngularFrameworkDelegate {
constructor(private crf: ComponentFactoryResolver, private zone: NgZone) {
}
attachViewToDom(componentToMount: Type<any>, viewport: ViewContainerRef, providers: any, changeDetection: ChangeDetectorRef): Promise<AngularMountingData> {
return new Promise((resolve) => {
this.zone.run(() => {
const mountingData = attachViewToDom(this.crf, componentToMount, viewport, providers, changeDetection);
resolve(mountingData);
});
});
}
removeViewFromDom(componentRef: ComponentRef<any>): Promise<any> {
return new Promise((resolve) => {
this.zone.run(() => {
componentRef.destroy();
resolve();
});
});
}
}
export function attachViewToDom(crf: ComponentFactoryResolver, componentToMount: Type<any>, viewport: ViewContainerRef, providers: any, changeDetection: ChangeDetectorRef): AngularMountingData{
const componentFactory = crf.resolveComponentFactory(componentToMount);
const componentProviders = ReflectiveInjector.resolve(providers);
const childInjector = ReflectiveInjector.fromResolvedProviders(componentProviders, viewport.parentInjector);
const componentRef = componentFactory.create(childInjector, []);
viewport.insert(componentRef.hostView, viewport.length);
changeDetection.detectChanges();
return {
componentFactory,
childInjector: childInjector,
componentRef: componentRef
}
}
export interface AngularMountingData {
componentFactory: ComponentFactory<any>;
childInjector: Injector;
componentRef: ComponentRef<any>;
}

View File

@ -1,13 +1,8 @@
import { Injectable } from '@angular/core';
import { NavContainer } from '@ionic/core';
import { hydrateElement } from '../util/util';
@Injectable()
export class App {
private element: HTMLIonAppElement;
constructor() {
this.element = document.querySelector('ion-app') as HTMLIonAppElement;
constructor(public _element: HTMLIonAppElement) {
}
setTitle(title: string) {
@ -15,30 +10,46 @@ export class App {
}
isScrolling(): boolean {
if (this.element.isScrolling) {
return this.element.isScrolling();
}
return false;
return isScrollingImpl(this);
}
getRootNavs(): NavContainer[] {
if (this.element.getRootNavs) {
return this.element.getRootNavs();
}
return [];
return getRootNavsImpl(this);
}
getActiveNavs(rootNavId?: number): NavContainer[] {
if (this.element.getActiveNavs) {
return this.element.getActiveNavs(rootNavId);
}
return [];
return getActiveNavsImpl(this, rootNavId);
}
getNavByIdOrName(nameOrId: number | string): NavContainer {
if (this.element.getNavByIdOrName) {
return this.element.getNavByIdOrName(nameOrId);
}
return null;
return getNavByIdOrNameImpl(this, nameOrId);
}
}
export function isScrollingImpl(app: App) {
if (app._element && app._element.isScrolling) {
return app._element.isScrolling();
}
return false;
}
export function getRootNavsImpl(app: App) {
if (app._element && app._element.getRootNavs) {
return app._element.getRootNavs();
}
return [];
}
export function getActiveNavsImpl(app: App, rootNavId?: number): NavContainer[] {
if (app._element && app._element.getActiveNavs) {
return app._element.getActiveNavs(rootNavId);
}
return [];
}
export function getNavByIdOrNameImpl(app: App, nameOrId: number | string): NavContainer {
if (app._element && app._element.getNavByIdOrName) {
return app._element.getNavByIdOrName(nameOrId);
}
return null;
}

View File

@ -0,0 +1,104 @@
import { Injectable } from '@angular/core';
import { LoadingDismissEvent, LoadingOptions } from '@ionic/core';
import { ensureElementInBody, hydrateElement } from '../util/util';
let loadingId = 0;
@Injectable()
export class LoadingController {
create(opts?: LoadingOptions): LoadingProxy {
return getLoadingProxy(opts);
}
}
export function getLoadingProxy(opts: LoadingOptions){
return {
id: loadingId++,
state: PRESENTING,
opts: opts,
present: function() { return present(this)},
dismiss: function() { return dismiss(this)},
onDidDismiss: function(callback: (data: any, role: string) => void) {
(this as LoadingProxyInternal).onDidDismissHandler = callback;
},
onWillDismiss: function(callback: (data: any, role: string) => void) {
(this as LoadingProxyInternal).onWillDismissHandler = callback;
},
}
}
export function present(loadingProxy: LoadingProxyInternal): Promise<any> {
loadingProxy.state = PRESENTING;
return loadOverlay(loadingProxy.opts).then((loadingElement: HTMLIonLoadingElement) => {
loadingProxy.element = loadingElement;
const onDidDismissHandler = (event: LoadingDismissEvent) => {
loadingElement.removeEventListener(ION_LOADING_DID_DISMISS_EVENT, onDidDismissHandler);
if (loadingProxy.onDidDismissHandler) {
loadingProxy.onDidDismissHandler(event.detail.data, event.detail.role);
}
};
const onWillDismissHandler = (event: LoadingDismissEvent) => {
loadingElement.removeEventListener(ION_LOADING_WILL_DISMISS_EVENT, onWillDismissHandler);
if (loadingProxy.onWillDismissHandler) {
loadingProxy.onWillDismissHandler(event.detail.data, event.detail.role);
}
};
loadingElement.addEventListener(ION_LOADING_DID_DISMISS_EVENT, onDidDismissHandler);
loadingElement.addEventListener(ION_LOADING_WILL_DISMISS_EVENT, onWillDismissHandler);
if (loadingProxy.state === PRESENTING) {
return loadingElement.present();
}
// we'll only ever get here if someone tried to dismiss the overlay or mess with it's internal state
// attribute before it could async load and present itself.
// with that in mind, just return null to make the TS compiler happy
return null;
});
}
export function dismiss(loadingProxy: LoadingProxyInternal): Promise<any> {
loadingProxy.state = DISMISSING;
if (loadingProxy.element) {
if (loadingProxy.state === DISMISSING) {
return loadingProxy.element.dismiss();
}
}
// either we're not in the dismissing state
// or we're calling this before the element is created
// so just return a resolved promise
return Promise.resolve();
}
export function loadOverlay(opts: LoadingOptions): Promise<HTMLIonLoadingElement> {
const element = ensureElementInBody('ion-loading-controller') as HTMLIonLoadingControllerElement;
return hydrateElement(element).then(() => {
return element.create(opts);
});
}
export interface LoadingProxy {
present(): Promise<void>
dismiss(): Promise<void>
onDidDismiss(callback: (data: any, role: string) => void): void;
onWillDismiss(callback: (data: any, role: string) => void): void;
}
export interface LoadingProxyInternal extends LoadingProxy {
id: number;
opts: LoadingOptions;
state: number;
element: HTMLIonLoadingElement;
onDidDismissHandler?: (data: any, role: string) => void;
onWillDismissHandler?: (data: any, role: string) => void;
}
export const PRESENTING = 1;
export const DISMISSING = 2;
const ION_LOADING_DID_DISMISS_EVENT = 'ionLoadingDidDismiss';
const ION_LOADING_WILL_DISMISS_EVENT = 'ionLoadingWillDismiss';

View File

@ -1,4 +1,3 @@
import { Injectable } from '@angular/core';
import { NavOptions, PublicNav, PublicViewController } from '@ionic/core';
import { hydrateElement } from '../util/util';

View File

@ -0,0 +1,104 @@
import { Injectable } from '@angular/core';
import { ToastDismissEvent, ToastOptions } from '@ionic/core';
import { ensureElementInBody, hydrateElement } from '../util/util';
let toastId = 0;
@Injectable()
export class ToastController {
create(opts?: ToastOptions): ToastProxy {
return getToastProxy(opts);
}
}
export function getToastProxy(opts: ToastOptions){
return {
id: toastId++,
state: PRESENTING,
opts: opts,
present: function() { return present(this)},
dismiss: function() { return dismiss(this)},
onDidDismiss: function(callback: (data: any, role: string) => void) {
(this as ToastProxyInternal).onDidDismissHandler = callback;
},
onWillDismiss: function(callback: (data: any, role: string) => void) {
(this as ToastProxyInternal).onWillDismissHandler = callback;
},
}
}
export function present(toastProxy: ToastProxyInternal): Promise<any> {
toastProxy.state = PRESENTING;
return loadOverlay(toastProxy.opts).then((toastElement: HTMLIonToastElement) => {
toastProxy.element = toastElement;
const onDidDismissHandler = (event: ToastDismissEvent) => {
toastElement.removeEventListener(ION_TOAST_DID_DISMISS_EVENT, onDidDismissHandler);
if (toastProxy.onDidDismissHandler) {
toastProxy.onDidDismissHandler(event.detail.data, event.detail.role);
}
};
const onWillDismissHandler = (event: ToastDismissEvent) => {
toastElement.removeEventListener(ION_TOAST_WILL_DISMISS_EVENT, onWillDismissHandler);
if (toastProxy.onWillDismissHandler) {
toastProxy.onWillDismissHandler(event.detail.data, event.detail.role);
}
};
toastElement.addEventListener(ION_TOAST_DID_DISMISS_EVENT, onDidDismissHandler);
toastElement.addEventListener(ION_TOAST_WILL_DISMISS_EVENT, onWillDismissHandler);
if (toastProxy.state === PRESENTING) {
return toastElement.present();
}
// we'll only ever get here if someone tried to dismiss the overlay or mess with it's internal state
// attribute before it could async load and present itself.
// with that in mind, just return null to make the TS compiler happy
return null;
});
}
export function dismiss(toastProxy: ToastProxyInternal): Promise<any> {
toastProxy.state = DISMISSING;
if (toastProxy.element) {
if (toastProxy.state === DISMISSING) {
return toastProxy.element.dismiss();
}
}
// either we're not in the dismissing state
// or we're calling this before the element is created
// so just return a resolved promise
return Promise.resolve();
}
export function loadOverlay(opts: ToastOptions): Promise<HTMLIonToastElement> {
const element = ensureElementInBody('ion-toast-controller') as HTMLIonToastControllerElement;
return hydrateElement(element).then(() => {
return element.create(opts);
});
}
export interface ToastProxy {
present(): Promise<void>
dismiss(): Promise<void>
onDidDismiss(callback: (data: any, role: string) => void): void;
onWillDismiss(callback: (data: any, role: string) => void): void;
}
export interface ToastProxyInternal extends ToastProxy {
id: number;
opts: ToastOptions;
state: number;
element: HTMLIonToastElement;
onDidDismissHandler?: (data: any, role: string) => void;
onWillDismissHandler?: (data: any, role: string) => void;
}
export const PRESENTING = 1;
export const DISMISSING = 2;
const ION_TOAST_DID_DISMISS_EVENT = 'ionToastDidDismiss';
const ION_TOAST_WILL_DISMISS_EVENT = 'ionToastWillDismiss';

View File

@ -0,0 +1,16 @@
import {
ComponentFactory,
ComponentRef,
Injector
} from '@angular/core';
import { PublicViewController } from '@ionic/core';
export interface AngularViewController extends PublicViewController {
componentFactory?: ComponentFactory<any>;
injector?: Injector;
componentRef?: ComponentRef<any>;
instance?: any;
angularHostElement?: HTMLElement;
}