diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts index 4928b0673e..e2d7c192ad 100644 --- a/packages/angular/src/index.ts +++ b/packages/angular/src/index.ts @@ -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'; \ No newline at end of file diff --git a/packages/angular/src/module.ts b/packages/angular/src/module.ts index 8a6646dc7e..f79160907f 100644 --- a/packages/angular/src/module.ts +++ b/packages/angular/src/module.ts @@ -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 ] }; diff --git a/packages/angular/src/providers/popover-controller.ts b/packages/angular/src/providers/popover-controller.ts new file mode 100644 index 0000000000..accc93108e --- /dev/null +++ b/packages/angular/src/providers/popover-controller.ts @@ -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, _propsOrDataObj?: any, classesToAdd?: string[]): Promise { + + 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 { + 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 { + 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 { + const element = ensureElementInBody('ion-popover-controller') as HTMLIonPopoverControllerElement; + return hydrateElement(element).then(() => { + return element.create(opts); + }); +} + +export interface PopoverProxy { + present(): Promise; + dismiss(): Promise; + 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'; + diff --git a/packages/core/src/components.d.ts b/packages/core/src/components.d.ts index 102fb4a16f..4a4b828cab 100644 --- a/packages/core/src/components.d.ts +++ b/packages/core/src/components.d.ts @@ -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; } } } diff --git a/packages/core/src/components/popover/popover.tsx b/packages/core/src/components/popover/popover.tsx index 4a26d4caf9..0fefa40617 100644 --- a/packages/core/src/components/popover/popover.tsx +++ b/packages/core/src/components/popover/popover.tsx @@ -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 {
-
- +
@@ -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'; diff --git a/packages/core/src/components/popover/readme.md b/packages/core/src/components/popover/readme.md index 4ee5a72233..cfe0babf7d 100644 --- a/packages/core/src/components/popover/readme.md +++ b/packages/core/src/components/popover/readme.md @@ -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 diff --git a/packages/core/src/components/select/select.tsx b/packages/core/src/components/select/select.tsx index 531f1b551c..684942cf5e 100644 --- a/packages/core/src/components/select/select.tsx +++ b/packages/core/src/components/select/select.tsx @@ -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 { diff --git a/packages/demos/angular/src/app/app-routing.module.ts b/packages/demos/angular/src/app/app-routing.module.ts index 09e69ad51a..6f8d5b8879 100644 --- a/packages/demos/angular/src/app/app-routing.module.ts +++ b/packages/demos/angular/src/app/app-routing.module.ts @@ -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({ diff --git a/packages/demos/angular/src/app/home-page/home-page.component.html b/packages/demos/angular/src/app/home-page/home-page.component.html index 40bdd221d6..fd114e4099 100644 --- a/packages/demos/angular/src/app/home-page/home-page.component.html +++ b/packages/demos/angular/src/app/home-page/home-page.component.html @@ -27,4 +27,7 @@
  • Modal Page
  • +
  • + Popover Page +
  • diff --git a/packages/demos/angular/src/app/popover/popover-page-to-present.ts b/packages/demos/angular/src/app/popover/popover-page-to-present.ts new file mode 100644 index 0000000000..bceb59f09d --- /dev/null +++ b/packages/demos/angular/src/app/popover/popover-page-to-present.ts @@ -0,0 +1,35 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'page-one', + template: ` + + + Page One + + + + Page One +
      +
    • ngOnInit - {{ngOnInitDetection}}
    • +
    +
    + ` +}) +export class PopoverPageToPresent { + + ngOnInitDetection = 'initial'; + + constructor() { + + } + + + ngOnInit() { + console.log('page one ngOnInit'); + setInterval(() => { + this.ngOnInitDetection = '' + Date.now(); + }, 500); + } + +} diff --git a/packages/demos/angular/src/app/popover/popover-page.component.ts b/packages/demos/angular/src/app/popover/popover-page.component.ts new file mode 100644 index 0000000000..fbc79ffc86 --- /dev/null +++ b/packages/demos/angular/src/app/popover/popover-page.component.ts @@ -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: ` + + + + + Test + + + + Open Basic Popover + + + + ` +}) +export class PopoverPageComponent { + + constructor(private popoverController: PopoverController) { + } + + clickMe(event: Event) { + const popover = this.popoverController.create({ + component: PopoverPageToPresent, + ev: event + }); + return popover.present(); + } + +} diff --git a/packages/demos/angular/src/app/popover/popover-routing.module.ts b/packages/demos/angular/src/app/popover/popover-routing.module.ts new file mode 100644 index 0000000000..bad72d4bfa --- /dev/null +++ b/packages/demos/angular/src/app/popover/popover-routing.module.ts @@ -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 { } diff --git a/packages/demos/angular/src/app/popover/popover.module.ts b/packages/demos/angular/src/app/popover/popover.module.ts new file mode 100644 index 0000000000..2dd056e632 --- /dev/null +++ b/packages/demos/angular/src/app/popover/popover.module.ts @@ -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 { } diff --git a/packages/demos/react/src/pages/PageOne.js b/packages/demos/react/src/pages/PageOne.js index fc760cd28d..b148c1c668 100644 --- a/packages/demos/react/src/pages/PageOne.js +++ b/packages/demos/react/src/pages/PageOne.js @@ -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 { this.goToPageTwo()}>Go to Page Two
    - this.openModal()}>OpenModal + this.openModal()}>Open Modal +
    +
    + this.openPopover(event)}>Open Popover
    Some random content: {this.state.content} diff --git a/packages/demos/react/src/pages/PopoverPage.js b/packages/demos/react/src/pages/PopoverPage.js new file mode 100644 index 0000000000..dc8a38b869 --- /dev/null +++ b/packages/demos/react/src/pages/PopoverPage.js @@ -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 [ + this.element = element}> + + Popover Page + + , + + Popover Page +
    + this.dismiss()}>Dismiss +
    +
    + Some random content: {this.state.content} +
    +
    + Props : {this.props.paramOne} +
    +
    + ]; + } +} diff --git a/packages/react/src/apis/apis.ts b/packages/react/src/apis/apis.ts new file mode 100644 index 0000000000..c96fb500ae --- /dev/null +++ b/packages/react/src/apis/apis.ts @@ -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 { + return createOverlayInternal('ion-modal-controller', opts); +} + +export function createPopover(opts: ModalOptions): Promise { + 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); + }); +} \ No newline at end of file diff --git a/packages/react/src/apis/modal.ts b/packages/react/src/apis/modal.ts deleted file mode 100644 index deeb749381..0000000000 --- a/packages/react/src/apis/modal.ts +++ /dev/null @@ -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 { - opts.delegate = Delegate; - const element = getOrAppendElement('ion-modal-controller') as HTMLIonModalControllerElement; - return (element as any).componentOnReady().then(() => { - return element.create(opts); - }); -} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 53c8f6c1b7..e19cf69f08 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,3 +1,3 @@ export { Delegate } from './react-framework-delegate'; -export { createModal } from './apis/modal'; +export * from './apis/apis'; export * from './utils/wc-shim'; \ No newline at end of file