refactor(popover): get popover working with dom, react, angular components

This commit is contained in:
Dan Bucholtz
2017-12-14 16:21:03 -06:00
parent 85785b9cf7
commit 1ba73a5f29
18 changed files with 402 additions and 41 deletions

View File

@ -10,4 +10,5 @@ export { App } from './providers/app';
export { LoadingController, LoadingProxy } from './providers/loading-controller';
export { ModalController, ModalProxy } from './providers/modal-controller';
export { NavController } from './providers/nav-controller';
export { PopoverController, PopoverProxy } from './providers/popover-controller';
export { ToastController, ToastProxy } from './providers/toast-controller';

View File

@ -21,6 +21,7 @@ import { AlertController } from './providers/alert-controller';
import { AngularComponentMounter } from './providers/angular-component-mounter';
import { LoadingController } from './providers/loading-controller';
import { ModalController } from './providers/modal-controller';
import { PopoverController } from './providers/popover-controller';
import { ToastController } from './providers/toast-controller';
@NgModule({
@ -57,6 +58,7 @@ export class IonicAngularModule {
AngularComponentMounter,
LoadingController,
ModalController,
PopoverController,
ToastController
]
};

View File

@ -0,0 +1,134 @@
import {
ComponentFactoryResolver,
Injectable,
Injector,
Type,
} from '@angular/core';
import {
FrameworkDelegate,
PopoverDismissEvent,
PopoverOptions
} from '@ionic/core';
import { AngularComponentMounter } from '../providers/angular-component-mounter';
import { AngularMountingData } from '../types/interfaces';
import { ensureElementInBody, hydrateElement } from '../util/util';
let popoverId = 0;
@Injectable()
export class PopoverController implements FrameworkDelegate {
constructor(private angularComponentMounter: AngularComponentMounter, private componentResolveFactory: ComponentFactoryResolver, private injector: Injector) {
}
create(opts?: PopoverOptions): PopoverProxy {
opts.delegate = this;
return getPopoverProxy(opts);
}
attachViewToDom(elementOrContainerToMountTo: HTMLElement, elementOrComponentToMount: Type<any>, _propsOrDataObj?: any, classesToAdd?: string[]): Promise<AngularMountingData> {
const hostElement = document.createElement('div');
return this.angularComponentMounter.attachViewToDom(elementOrContainerToMountTo, hostElement, elementOrComponentToMount, this.componentResolveFactory, this.injector, classesToAdd);
}
removeViewFromDom(_parentElement: HTMLElement, childElement: HTMLElement) {
return this.angularComponentMounter.removeViewFromDom(childElement);
}
}
export function getPopoverProxy(opts: PopoverOptions) {
return {
id: popoverId++,
state: PRESENTING,
opts: opts,
present: function() { return present(this); },
dismiss: function() { return dismiss(this); },
onDidDismiss: function(callback: (data: any, role: string) => void) {
(this as PopoverProxyInternal).onDidDismissHandler = callback;
},
onWillDismiss: function(callback: (data: any, role: string) => void) {
(this as PopoverProxyInternal).onWillDismissHandler = callback;
},
};
}
export function present(popoverProxy: PopoverProxyInternal): Promise<any> {
popoverProxy.state = PRESENTING;
return loadOverlay(popoverProxy.opts).then((popoverElement: HTMLIonPopoverElement) => {
Object.assign(popoverElement, popoverProxy.opts);
popoverProxy.element = popoverElement;
const onDidDismissHandler = (event: PopoverDismissEvent) => {
popoverElement.removeEventListener(ION_POPOVER_DID_DISMISS_EVENT, onDidDismissHandler);
if (popoverProxy.onDidDismissHandler) {
popoverProxy.onDidDismissHandler(event.detail.data, event.detail.role);
}
};
const onWillDismissHandler = (event: PopoverDismissEvent) => {
popoverElement.removeEventListener(ION_POPOVER_WILL_DISMISS_EVENT, onWillDismissHandler);
if (popoverProxy.onWillDismissHandler) {
popoverProxy.onWillDismissHandler(event.detail.data, event.detail.role);
}
};
popoverElement.addEventListener(ION_POPOVER_DID_DISMISS_EVENT, onDidDismissHandler);
popoverElement.addEventListener(ION_POPOVER_WILL_DISMISS_EVENT, onWillDismissHandler);
if (popoverProxy.state === PRESENTING) {
return popoverElement.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(popoverProxy: PopoverProxyInternal): Promise<any> {
popoverProxy.state = DISMISSING;
if (popoverProxy.element) {
if (popoverProxy.state === DISMISSING) {
return popoverProxy.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: PopoverOptions): Promise<HTMLIonPopoverElement> {
const element = ensureElementInBody('ion-popover-controller') as HTMLIonPopoverControllerElement;
return hydrateElement(element).then(() => {
return element.create(opts);
});
}
export interface PopoverProxy {
present(): Promise<void>;
dismiss(): Promise<void>;
onDidDismiss(callback: (data: any, role: string) => void): void;
onWillDismiss(callback: (data: any, role: string) => void): void;
}
export interface PopoverProxyInternal extends PopoverProxy {
id: number;
opts: PopoverOptions;
state: number;
element: HTMLIonPopoverElement;
onDidDismissHandler?: (data: any, role: string) => void;
onWillDismissHandler?: (data: any, role: string) => void;
}
export const PRESENTING = 1;
export const DISMISSING = 2;
const ION_POPOVER_DID_DISMISS_EVENT = 'ionPopoverDidDismiss';
const ION_POPOVER_WILL_DISMISS_EVENT = 'ionPopoverWillDismiss';

View File

@ -1920,7 +1920,7 @@ declare global {
mode?: string;
color?: string;
component?: string;
componentProps?: any;
data?: any;
cssClass?: string;
enableBackdropDismiss?: boolean;
enterAnimation?: AnimationBuilder;
@ -1930,6 +1930,7 @@ declare global {
showBackdrop?: boolean;
translucent?: boolean;
animate?: boolean;
delegate?: FrameworkDelegate;
}
}
}

View File

@ -4,9 +4,12 @@ import {
AnimationBuilder,
AnimationController,
Config,
FrameworkDelegate,
OverlayDismissEvent,
OverlayDismissEventDetail
} from '../../index';
import { DomFrameworkDelegate } from '../../utils/dom-framework-delegate';
import { domControllerAsync, playAnimationAsync } from '../../utils/helpers';
import { createThemedClasses } from '../../utils/theme';
@ -26,7 +29,6 @@ import mdLeaveAnimation from './animations/md.leave';
}
})
export class Popover {
private animation: Animation;
@Element() private el: HTMLElement;
@ -66,7 +68,7 @@ export class Popover {
@Prop() mode: string;
@Prop() color: string;
@Prop() component: string;
@Prop() componentProps: any = {};
@Prop() data: any = {};
@Prop() cssClass: string;
@Prop() enableBackdropDismiss: boolean = true;
@Prop() enterAnimation: AnimationBuilder;
@ -76,7 +78,10 @@ export class Popover {
@Prop() showBackdrop: boolean = true;
@Prop() translucent: boolean = false;
@Prop() animate: boolean = true;
@Prop({ mutable: true }) delegate: FrameworkDelegate;
private animation: Animation;
private usersComponentElement: HTMLElement;
@Method()
present() {
@ -91,8 +96,24 @@ export class Popover {
// get the user's animation fn if one was provided
const animationBuilder = this.enterAnimation || this.config.get('popoverEnter', this.mode === 'ios' ? iosEnterAnimation : mdEnterAnimation);
// build the animation and kick it off
return this.animationCtrl.create(animationBuilder, this.el, this.ev).then(animation => {
const userComponentParent = this.el.querySelector(`.${USER_COMPONENT_POPOVER_CONTAINER_CLASS}`);
if (!this.delegate) {
this.delegate = new DomFrameworkDelegate();
}
const cssClasses: string[] = [];
if (this.cssClass && this.cssClass.length) {
cssClasses.push(this.cssClass);
}
// add the modal by default to the data being passed
this.data = this.data || {};
this.data.modal = this.el;
return this.delegate.attachViewToDom(userComponentParent, this.component,
this.data, cssClasses).then((mountingData) => {
this.usersComponentElement = mountingData.element;
return this.animationCtrl.create(animationBuilder, this.el, this.ev);
}).then((animation) => {
this.animation = animation;
if (!this.animate) {
// if the duration is 0, it won't actually animate I don't think
@ -100,7 +121,7 @@ export class Popover {
this.animation = animation.duration(0);
}
return playAnimationAsync(animation);
}).then((animation) => {
}).then((animation) => {
animation.destroy();
this.componentDidEnter();
});
@ -119,6 +140,10 @@ export class Popover {
role
});
if (!this.delegate) {
this.delegate = new DomFrameworkDelegate();
}
const animationBuilder = this.leaveAnimation || this.config.get('popoverLeave', this.mode === 'ios' ? iosLeaveAnimation : mdLeaveAnimation);
return this.animationCtrl.create(animationBuilder, this.el).then(animation => {
@ -126,10 +151,13 @@ export class Popover {
return playAnimationAsync(animation);
}).then((animation) => {
animation.destroy();
return domControllerAsync(Context.dom.write, () => {
this.el.parentNode.removeChild(this.el);
});
return domControllerAsync(Context.dom.write, () => {});
}).then(() => {
// TODO - Figure out how to make DOM controller work with callbacks that return a promise or are async
const userComponentParent = this.el.querySelector(`.${USER_COMPONENT_POPOVER_CONTAINER_CLASS}`);
return this.delegate.removeViewFromDom(userComponentParent, this.usersComponentElement);
}).then(() => {
this.el.parentElement.removeChild(this.el);
this.ionPopoverDidDismiss.emit({
data,
role
@ -179,7 +207,6 @@ export class Popover {
}
render() {
const ThisComponent = this.component;
const wrapperClasses = createThemedClasses(this.mode, this.color, 'popover-wrapper');
return [
@ -190,11 +217,7 @@ export class Popover {
<div class={wrapperClasses}>
<div class='popover-arrow'/>
<div class='popover-content'>
<div class='popover-viewport'>
<ThisComponent
{...this.componentProps}
class={this.cssClass}
/>
<div class={USER_COMPONENT_POPOVER_CONTAINER_CLASS}>
</div>
</div>
</div>
@ -203,8 +226,8 @@ export class Popover {
}
export interface PopoverOptions {
component: string;
componentProps?: any;
component: any;
data?: any;
showBackdrop?: boolean;
enableBackdropDismiss?: boolean;
translucent?: boolean;
@ -212,6 +235,7 @@ export interface PopoverOptions {
leavenimation?: AnimationBuilder;
cssClass?: string;
ev: Event;
delegate?: FrameworkDelegate;
}
export interface PopoverEvent extends CustomEvent {
@ -251,3 +275,5 @@ export {
mdEnterAnimation as mdPopoverEnterAnimation,
mdLeaveAnimation as mdPopoverLeaveAnimation
};
export const USER_COMPONENT_POPOVER_CONTAINER_CLASS = 'popover-viewport';

View File

@ -22,14 +22,19 @@ string
string
#### componentProps
#### cssClass
string
#### data
any
#### cssClass
#### delegate
string
any
#### enableBackdropDismiss
@ -89,14 +94,19 @@ string
string
#### componentProps
#### cssClass
string
#### data
any
#### cssClass
#### delegate
string
any
#### enableBackdropDismiss

View File

@ -287,7 +287,7 @@ export class Select {
openPopover(ev: UIEvent) {
const popoverOpts: PopoverOptions = {
component: 'ion-select-popover',
componentProps: {
data: {
value: this.value,
options: this.childOpts.map(o => {
return {

View File

@ -11,7 +11,8 @@ const routes: Routes = [
{ path: 'toast', loadChildren: 'app/toast/toast.module#ToastModule' },
{ path: 'loading', loadChildren: 'app/loading/loading.module#LoadingModule' },
{ path: 'nav', loadChildren: 'app/nav/nav.module#NavModule' },
{ path: 'modal', loadChildren: 'app/modal/modal.module#ModalModule' }
{ path: 'modal', loadChildren: 'app/modal/modal.module#ModalModule' },
{ path: 'popover', loadChildren: 'app/popover/popover.module#PopoverModule' },
];
@NgModule({

View File

@ -27,4 +27,7 @@
<li>
<a href='modal'>Modal Page</a>
</li>
<li>
<a href='popover'>Popover Page</a>
</li>
</ul>

View File

@ -0,0 +1,35 @@
import { Component } from '@angular/core';
@Component({
selector: 'page-one',
template: `
<ion-header>
<ion-toolbar>
<ion-title>Page One</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
Page One
<ul>
<li>ngOnInit - {{ngOnInitDetection}}</li>
</ul>
</ion-content>
`
})
export class PopoverPageToPresent {
ngOnInitDetection = 'initial';
constructor() {
}
ngOnInit() {
console.log('page one ngOnInit');
setInterval(() => {
this.ngOnInitDetection = '' + Date.now();
}, 500);
}
}

View File

@ -0,0 +1,36 @@
import { Component } from '@angular/core';
import { PopoverController } from '@ionic/angular';
import { PopoverPageToPresent } from './popover-page-to-present';
@Component({
selector: 'app-popover-page',
template: `
<ion-app>
<ion-page class="show-page">
<ion-header>
<ion-toolbar>
<ion-title>Test</ion-title>
</ion-toolbar>
</ion-header>
<ion-content padding>
<ion-button (click)="clickMe($event)">Open Basic Popover</ion-button>
</ion-content>
</ion-page>
</ion-app>
`
})
export class PopoverPageComponent {
constructor(private popoverController: PopoverController) {
}
clickMe(event: Event) {
const popover = this.popoverController.create({
component: PopoverPageToPresent,
ev: event
});
return popover.present();
}
}

View File

@ -0,0 +1,14 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { PopoverPageComponent } from './popover-page.component';
const routes: Routes = [
{ path: '', component: PopoverPageComponent }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class PopoverRoutingModule { }

View File

@ -0,0 +1,27 @@
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { CommonModule } from '@angular/common';
import { IonicAngularModule } from '@ionic/angular';
import { PopoverPageComponent } from './popover-page.component';
import { PopoverRoutingModule } from './popover-routing.module';
import { PopoverPageToPresent } from './popover-page-to-present';
@NgModule({
imports: [
CommonModule,
IonicAngularModule.forRoot(),
PopoverRoutingModule
],
declarations: [
PopoverPageComponent,
PopoverPageToPresent
],
providers: [
],
entryComponents: [
PopoverPageToPresent
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class PopoverModule { }

View File

@ -1,9 +1,10 @@
import React, { Component } from 'react';
import { createModal } from '@ionic/react';
import { createModal, createPopover } from '@ionic/react';
import PageTwo from './PageTwo';
import ModalPage from './ModalPage';
import PopoverPage from './PopoverPage';
export default class PageOne extends Component {
@ -46,6 +47,15 @@ export default class PageOne extends Component {
});
}
openPopover(event) {
return createPopover({
component: PopoverPage,
ev: event
}).then((popover) => {
return popover.present();
});
}
componentDidMount() {
setInterval(() => {
this.setState({ content: Math.random() * 1000});
@ -66,7 +76,10 @@ export default class PageOne extends Component {
<ion-button onClick={() => this.goToPageTwo()}>Go to Page Two</ion-button>
</div>
<div>
<ion-button onClick={() => this.openModal()}>OpenModal</ion-button>
<ion-button onClick={() => this.openModal()}>Open Modal</ion-button>
</div>
<div>
<ion-button onClick={(event) => this.openPopover(event)}>Open Popover</ion-button>
</div>
<div>
Some random content: {this.state.content}

View File

@ -0,0 +1,49 @@
import React, { Component } from 'react';
import PageThree from './PageThree';
export default class PopoverPage extends Component {
constructor() {
super();
this.style = {
height: '100%'
};
this.state = {
content: 'popover page - ' + 50
}
}
componentDidMount() {
console.log('componentDidMount');
setInterval(() => {
this.setState({ content: 'Popover page - ' + Math.random() * 1000});
}, 1000);
}
dismiss() {
return this.props.popover.dismiss();
}
render() {
return [
<ion-header ref={(element) => this.element = element}>
<ion-toolbar>
<ion-title>Popover Page</ion-title>
</ion-toolbar>
</ion-header>,
<ion-content>
Popover Page
<div>
<ion-button onClick={() => this.dismiss()}>Dismiss</ion-button>
</div>
<div>
Some random content: {this.state.content}
</div>
<div>
Props : {this.props.paramOne}
</div>
</ion-content>
];
}
}

View File

@ -0,0 +1,22 @@
import { ModalOptions } from '@ionic/core';
import { Delegate } from '../react-framework-delegate';
import { getOrAppendElement } from '../utils/helpers';
export function createModal(opts: ModalOptions): Promise<HTMLIonModalElement> {
return createOverlayInternal('ion-modal-controller', opts);
}
export function createPopover(opts: ModalOptions): Promise<HTMLIonModalElement> {
return createOverlayInternal('ion-popover-controller', opts);
}
function createOverlayInternal(controllerTagName: string, opts: any) {
opts.delegate = Delegate;
const element = getOrAppendElement(controllerTagName) as HTMLIonModalControllerElement;
return (element as any).componentOnReady().then(() => {
return element.create(opts);
});
}

View File

@ -1,13 +0,0 @@
import { ModalOptions } from '@ionic/core';
import { Delegate } from '../react-framework-delegate';
import { getOrAppendElement } from '../utils/helpers';
export function createModal(opts: ModalOptions): Promise<HTMLIonModalElement> {
opts.delegate = Delegate;
const element = getOrAppendElement('ion-modal-controller') as HTMLIonModalControllerElement;
return (element as any).componentOnReady().then(() => {
return element.create(opts);
});
}

View File

@ -1,3 +1,3 @@
export { Delegate } from './react-framework-delegate';
export { createModal } from './apis/modal';
export * from './apis/apis';
export * from './utils/wc-shim';