diff --git a/.gitignore b/.gitignore index da38761..17dd3b5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,11 +2,11 @@ ~* dist/ +types/ node_modules/ coverage/ reports/ src/**/*.js -types/**/*.map # ignore log files *.log diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..2c2c282 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "semi": false, + "singleQuote": true, + "printWidth": 80 +} diff --git a/README.md b/README.md index e580a17..9ea438d 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,3 @@ -# Ionic-Vue becomes @ionic/vue -**Important: This project has been contributed to the Ionic core and can be used as [@ionic/vue](https://github.com/ionic-team/ionic/tree/master/vue).** - -Modus Create engineers will continue to support the community at the Ionic's official [Issue board](https://github.com/ionic-team/ionic/issues) - -However, this repository is still being actively maintained and kept in-sync with the official @ionic/vue. The main difference being the availability of pending upstream pull requests and flexibility of choosing dependency versions. - -Our goal is to allow developers to be on the bleeding-edge and freely experiment, thus we are delivering features and bug fixes as fast as possible. - -Bug fixes, features, documentation and any other changes are always contributed back to upstream @ionic/vue. - ---- ---- # Ionic-Vue @@ -26,6 +13,16 @@ Ionic integration adapters for Vue. @modus/ionic-vue

+## `Ionic-Vue` vs `@ionic/vue` +`Ionic-Vue` codebase has been contributed to the Ionic core and as [@ionic/vue](https://github.com/ionic-team/ionic/tree/master/vue). However, `@ionic/vue` provides limited support to Ionic v4. + +The amazing Ionic team is always looking to bring the most modern of browser capabilities to their framework. The official Vue support will land after Vue 3 has stabilized. + +Modus Create engineers continue to maintain this library to support the community that wants to create beautiful mobile apps with Vue and Ionic. + +Our goal is to allow developers to be up to date with the latest advances of Ionic and Vue. Thus we are delivering features and bug fixes as fast as possible. + + ## Roadmap Overview: all of the controllers and major features such as transitions and router have been implemented and tested for several months now. @@ -42,11 +39,15 @@ Apart from minor improvements and further testing of various mixes of Ionic comp | Router keep-alive | :heavy_check_mark: | [Pending](https://github.com/ionic-team/ionic/pull/18561) | - | | Functional Inputs | :heavy_check_mark: | [Pending](https://github.com/ionic-team/ionic/pull/19087) | - | | Import controllers directly | :heavy_check_mark: | [Pending](https://github.com/ionic-team/ionic/pull/19573) | Improve treeshaking and sync with react and angular implementations | +| Restore scroll on navigation | :heavy_check_mark: | - | When going back through history restore the scroll position | | Unit tests | :x: | :x: | Outdated as were originally written in plain JS, need to be updated for TS | ## Ionic versions 4 and 5 :warning: Moving forward all versions of `ionic-vue` will be supporting Ionic 5 only, if you'd like to continue using Ionic 4 please use `ionic-vue` version `1.3.4` +## Vue 3 +:construction: We are actively developing the next major version of this library. It supports Vue 3 and all of the new APIs like Composition, new transition features, etc. You can [track the progress in the dev branch](https://github.com/ModusCreateOrg/ionic-vue/tree/dev). + ## Installing / Getting started A quick introduction of the minimal setup you need to get a hello world up and running. @@ -119,14 +120,14 @@ Vue.component('Foo', { IonicVue supports all of the Ionic controllers: -- [Action Sheet](https://github.com/ionic-team/ionic/tree/master/core/src/components/action-sheet-controller) -- [Alert](https://github.com/ionic-team/ionic/tree/master/core/src/components/alert-controller) -- [Loading](https://github.com/ionic-team/ionic/tree/master/core/src/components/loading-controller) -- [Menu](https://github.com/ionic-team/ionic/tree/master/core/src/components/menu-controller) -- [Modal](https://github.com/ionic-team/ionic/tree/master/core/src/components/modal-controller) -- [Picker](https://github.com/ionic-team/ionic/tree/master/core/src/components/picker-controller) -- [Popover](https://github.com/ionic-team/ionic/tree/master/core/src/components/popover-controller) -- [Toast](https://github.com/ionic-team/ionic/tree/master/core/src/components/toast-controller) +- [Action Sheet](https://github.com/ionic-team/ionic/tree/master/core/src/components/action-sheet) +- [Alert](https://github.com/ionic-team/ionic/tree/master/core/src/components/alert) +- [Loading](https://github.com/ionic-team/ionic/tree/master/core/src/components/loading) +- [Menu](https://github.com/ionic-team/ionic/tree/master/core/src/components/menu) +- [Modal](https://github.com/ionic-team/ionic/tree/master/core/src/components/modal) +- [Picker](https://github.com/ionic-team/ionic/tree/master/core/src/components/picker) +- [Popover](https://github.com/ionic-team/ionic/tree/master/core/src/components/popover) +- [Toast](https://github.com/ionic-team/ionic/tree/master/core/src/components/toast) ### IonicVueRouter diff --git a/build/release.sh b/build/release.sh index 32fe505..3fa2b89 100755 --- a/build/release.sh +++ b/build/release.sh @@ -5,19 +5,21 @@ echo "Enter release version: " read VERSION read -p "Releasing $VERSION - are you sure? (y/n)" -n 1 -r -echo # (optional) move to a new line +echo # move to new line + if [[ $REPLY =~ ^[Yy]$ ]] then echo "Releasing $VERSION ..." VERSION=$VERSION npm run prod # commit + npm version $VERSION --no-git-tag-version git add -A - git commit --allow-empty -m "[build] $VERSION" - npm version $VERSION --message "[release] $VERSION" + git commit -m "[build] $VERSION" + git tag v$VERSION # publish - git push origin refs/tags/v$VERSION git push + git push --tags npm publish fi diff --git a/package-lock.json b/package-lock.json index 10f06d2..c227477 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@modus/ionic-vue", - "version": "1.3.5", + "version": "1.3.12", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index d49cca3..06659f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@modus/ionic-vue", - "version": "1.3.5", + "version": "1.3.12", "description": "Ionic integration adapters for Vue", "homepage": "https://moduscreate.com", "author": "Michael Tintiuc ", @@ -49,9 +49,8 @@ "scripts": { "dev": "rollup -c ./build/rollup.config.js", "watch": "rollup -w -c ./build/rollup.config.js", - "prod": "npm run clean && NODE_ENV=production rollup -c ./build/rollup.config.js --configProd", - "lint": "tslint --project .", - "lint.fix": "tslint --project . --fix", + "prod": "npm run lint && npm run clean && NODE_ENV=production rollup -c ./build/rollup.config.js --configProd", + "lint": "tslint --project . --fix", "test": "jest --coverage --verbose", "install.peer": "npm i --no-save vue vue-template-compiler vue-router @ionic/core", "clean": "node ./scripts/clean.js" diff --git a/src/app-initialize.ts b/src/app-initialize.ts deleted file mode 100644 index d6b7dbb..0000000 --- a/src/app-initialize.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { IonicConfig } from '@ionic/core'; -import { applyPolyfills, defineCustomElements } from '@ionic/core/loader'; -import { IonicWindow } from './interfaces'; - -export async function appInitialize(config?: IonicConfig) { - const win: IonicWindow = window as any; - const Ionic = win?.Ionic || {}; - - Ionic.config = config; - - await applyPolyfills(); - defineCustomElements(win); -} diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..1fb9cc0 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,3 @@ +export * from './overlays'; +export * from './inputs'; +export * from './navigation'; diff --git a/src/components/inputs.ts b/src/components/inputs.ts index 0b9a7ca..f2312d4 100644 --- a/src/components/inputs.ts +++ b/src/components/inputs.ts @@ -1,83 +1,39 @@ -import Vue, { CreateElement, RenderContext } from 'vue'; - -interface EventHandler { - [key: string]: (e: Event) => void; -} - -type Callback = (value: Event | string) => void; - -// Events to register handlers for -const events: string[] = ['ionChange', 'ionInput', 'ionBlur', 'ionFocus', 'ionCancel', 'ionSelect']; - -// Arguments to be passed to the factory function -const inputComponentsToBeCreated = [ - ['IonCheckboxVue', 'ion-checkbox', 'ionChange', 'checked'], - ['IonDatetimeVue', 'ion-datetime'], - ['IonInputVue', 'ion-input', 'ionInput'], - ['IonRadioVue', 'ion-radio', 'ionSelect'], - ['IonRangeVue', 'ion-range'], - ['IonSearchbarVue', 'ion-searchbar', 'ionInput'], - ['IonSelectVue', 'ion-select'], - ['IonTextareaVue', 'ion-textarea'], - ['IonToggleVue', 'ion-toggle', 'ionChange', 'checked'], -]; - -// Factory function for creation of input components -export function createInputComponents() { - inputComponentsToBeCreated.map(args => (createInputComponent as any)(...args)); -} - -/** - * Create a wrapped input component that captures typical ionic input events - * and emits core ones so v-model works. - * @param name the vue name of the component - * @param coreTag the actual tag to render (such as ion-datetime) - * @param modelEvent to be used for v-model - * @param valueProperty to be used for v-model - */ -function createInputComponent(name: string, coreTag: string, modelEvent = 'ionChange', valueProperty = 'value') { - Vue.component(name, { - name, - functional: true, - model: { - event: modelEvent, - prop: valueProperty, - }, - render(h: CreateElement, { data, listeners, slots }: RenderContext) { - return h(coreTag, { - ...data, - on: buildEventHandlers(listeners, modelEvent, valueProperty), - }, slots().default); - }, - }); -} - -function buildEventHandlers(listeners: RenderContext['listeners'], modelEvent: string, valueProperty: string) { - const handlers: EventHandler = {}; - - // Loop through all the events - events.map((eventName: string) => { - if (!listeners[eventName]) { - return; - } - - // Normalize listeners coming from context as Function | Function[] - const callbacks: Callback[] = Array.isArray(listeners[eventName]) - ? listeners[eventName] as Callback[] - : [listeners[eventName] as Callback]; - - // Assign handlers - handlers[eventName] = (e: Event) => { - callbacks.map(f => { - if (e) { - f(modelEvent === eventName - ? (e.target as any)[valueProperty] - : e - ); - } - }); - }; - }); - - return handlers; -} +import { createInputComponent } from '../utils'; + +export const IonCheckboxVue = createInputComponent( + 'IonCheckboxVue', + 'ion-checkbox', + 'ionChange', + 'checked' +); +export const IonDatetimeVue = createInputComponent( + 'IonDatetimeVue', + 'ion-datetime' +); +export const IonInputVue = createInputComponent( + 'IonInputVue', + 'ion-input', + 'ionInput' +); +export const IonRadioVue = createInputComponent( + 'IonRadioVue', + 'ion-radio', + 'ionSelect' +); +export const IonRangeVue = createInputComponent('IonRangeVue', 'ion-range'); +export const IonSearchbarVue = createInputComponent( + 'IonSearchbarVue', + 'ion-searchbar', + 'ionInput' +); +export const IonSelectVue = createInputComponent('IonSelectVue', 'ion-select'); +export const IonTextareaVue = createInputComponent( + 'IonTextareaVue', + 'ion-textarea' +); +export const IonToggleVue = createInputComponent( + 'IonToggleVue', + 'ion-toggle', + 'ionChange', + 'checked' +); diff --git a/src/components/ion-vue-router.ts b/src/components/ion-vue-router.ts index 55cf923..b3e85fe 100644 --- a/src/components/ion-vue-router.ts +++ b/src/components/ion-vue-router.ts @@ -3,17 +3,18 @@ import { NavDirection } from '@ionic/core'; type TransitionDone = () => void; -interface Props { - name: string; - animated: boolean; -} - interface KeepAliveProps { include?: string | string[] | RegExp; exclude?: string | string[] | RegExp; max?: number; } +interface Props { + name?: string; + animated?: boolean; + keepAlive?: KeepAliveProps; +} + // Component entering the view let enteringEl: HTMLElement; @@ -27,18 +28,20 @@ export default { // Disable transitions animated: { default: true, type: Boolean }, // keep-alive props - keepAlive: { type: [String, Object as () => KeepAliveProps] }, + keepAlive: { type: [String, Object as () => KeepAliveProps] } }, render(h: CreateElement, { parent, props, data, children }: RenderContext) { if (!parent.$router) { - throw new Error('IonTabs requires an instance of either VueRouter or IonicVueRouter'); + throw new Error( + 'IonVueRouter requires an instance of either VueRouter or IonicVueRouter' + ); } const ionRouterOutletData: VNodeData = { ...data, ref: 'ionRouterOutlet', - on: { click: (event: Event) => catchIonicGoBack(parent, event) }, + on: { click: (event: Event) => catchIonicGoBack(parent, event) } }; const routerViewData: VNodeData = { props: { name: props.name } }; const transitionData: VNodeData = { @@ -47,18 +50,26 @@ export default { leave: (el: HTMLElement, done: TransitionDone) => { leave(parent, props as Props, el, done); }, + enter: (el: HTMLElement, done: TransitionDone) => { + enter(parent, el, done); + }, beforeEnter, - enter, afterEnter, beforeLeave, afterLeave, enterCancelled, - leaveCancelled, + leaveCancelled } }; const routerViewNode: VNode = h('router-view', routerViewData, children); - const keepAliveNode: VNode = h('keep-alive', { props: { ...props.keepAlive } }, [routerViewNode]); - const transitionNode: VNode = h('transition', transitionData, [props.keepAlive === undefined ? routerViewNode : keepAliveNode]); + const keepAliveNode: VNode = h( + 'keep-alive', + { props: { ...props.keepAlive } }, + [routerViewNode] + ); + const transitionNode: VNode = h('transition', transitionData, [ + props.keepAlive === undefined ? routerViewNode : keepAliveNode + ]); return h('ion-router-outlet', ionRouterOutletData, [transitionNode]); } @@ -71,7 +82,9 @@ function catchIonicGoBack(parent: Vue, event: Event): void { if (!event.target) return; // We only care for the event coming from Ionic's back button - const backButton = (event.target as HTMLElement).closest('ion-back-button') as HTMLIonBackButtonElement; + const backButton = (event.target as HTMLElement).closest( + 'ion-back-button' + ) as HTMLIonBackButtonElement; if (!backButton) return; const $router = parent.$router; @@ -96,27 +109,26 @@ function catchIonicGoBack(parent: Vue, event: Event): void { } // Transition when we leave the route -function leave(parent: Vue, props: Props, el: HTMLElement, done: TransitionDone) { - const promise = transition(parent, props, el); - - // Skip any transition if we don't get back a Promise - if (!promise) { - done(); - return; - } +async function leave( + parent: Vue, + props: Props, + el: HTMLElement, + done: TransitionDone +) { + await parent.$router.saveScroll(el); // Perform navigation once the transition was finished - parent.$router.transition = new Promise(resolve => { - promise.then(() => { - resolve(); - done(); - }).catch(console.error); + parent.$router.transition = new Promise(async resolve => { + await transition(parent, props, el); + done(); + resolve(); }); } // Trigger the ionic/core transitions function transition(parent: Vue, props: Props, leavingEl: HTMLElement) { - const ionRouterOutlet = parent.$refs.ionRouterOutlet as HTMLIonRouterOutletElement; + const ionRouterOutlet = parent.$refs + .ionRouterOutlet as HTMLIonRouterOutletElement; // The Ionic framework didn't load - skip animations if (typeof ionRouterOutlet.componentOnReady === 'undefined') { @@ -132,15 +144,27 @@ function transition(parent: Vue, props: Props, leavingEl: HTMLElement) { // Add the proper Ionic classes, important for smooth transitions enteringEl.classList.add('ion-page', 'ion-page-invisible'); + // Reset the cached styling applied by ionic/core's: + // .afterStyles({ 'display': 'none' }) + if (typeof props.keepAlive !== 'undefined') { + enteringEl.style.display = ''; + } + // Commit to the transition as soon as the Ionic Router Outlet is ready - return ionRouterOutlet.componentOnReady().then((el: HTMLIonRouterOutletElement) => { - return el.commit(enteringEl, leavingEl, { - deepWait: true, - duration: !props.animated || parent.$router.direction === 'root' ? 0 : undefined, - direction: parent.$router.direction as NavDirection, - showGoBack: parent.$router.canGoBack(), - }); - }).catch(console.error); + return ionRouterOutlet + .componentOnReady() + .then((el: HTMLIonRouterOutletElement) => { + return el.commit(enteringEl, leavingEl, { + deepWait: true, + duration: + !props.animated || parent.$router.direction === 'root' + ? 0 + : undefined, + direction: parent.$router.direction as NavDirection, + showGoBack: parent.$router.canGoBack() + }); + }) + .catch(console.error); } // Set the component to be rendered before we render the new route @@ -149,13 +173,29 @@ function beforeEnter(el: HTMLElement) { } // Enter the new route -function enter(_el: HTMLElement, done: TransitionDone) { - done(); +function enter(parent: Vue, el: HTMLElement, done: TransitionDone) { + if (parent.$router.direction === 'back') { + parent.$router + .restoreScroll(el, parent.$router.currentRoute.fullPath) + .then(done); + } else { + done(); + } } // Vue transition stub functions -function afterEnter(_el: HTMLElement) { /* */ } -function afterLeave(_el: HTMLElement) { /* */ } -function beforeLeave(_el: HTMLElement) { /* */ } -function enterCancelled(_el: HTMLElement) { /* */ } -function leaveCancelled(_el: HTMLElement) { /* */ } +function afterEnter(_el: HTMLElement) { + /* */ +} +function afterLeave(_el: HTMLElement) { + /* */ +} +function beforeLeave(_el: HTMLElement) { + /* */ +} +function enterCancelled(_el: HTMLElement) { + /* */ +} +function leaveCancelled(_el: HTMLElement) { + /* */ +} diff --git a/src/components/navigation/index.ts b/src/components/navigation/index.ts new file mode 100644 index 0000000..b2c620e --- /dev/null +++ b/src/components/navigation/index.ts @@ -0,0 +1,2 @@ +export * from './ion-page'; +export * from './ion-tabs'; diff --git a/src/components/navigation/ion-page.ts b/src/components/navigation/ion-page.ts index 6230e7a..39d0e79 100644 --- a/src/components/navigation/ion-page.ts +++ b/src/components/navigation/ion-page.ts @@ -1,7 +1,7 @@ import { CreateElement, RenderContext } from 'vue'; -export default { - name: 'IonPage', +export const IonPageVue = { + name: 'IonPageVue', functional: true, render(h: CreateElement, { children }: RenderContext) { return h('div', { class: { 'ion-page': true } }, children); diff --git a/src/components/navigation/ion-tabs.ts b/src/components/navigation/ion-tabs.ts index 2909ac0..817e0bf 100644 --- a/src/components/navigation/ion-tabs.ts +++ b/src/components/navigation/ion-tabs.ts @@ -10,8 +10,8 @@ interface EventListeners { const tabBars = [] as VNode[]; const cachedTabs = [] as VNode[]; -export default { - name: 'IonTabs', +export const IonTabsVue = { + name: 'IonTabsVue', functional: true, render(h: CreateElement, { parent, data, slots, listeners }: RenderContext) { const renderQueue = [] as VNode[]; @@ -20,7 +20,7 @@ export default { let selectedTab = ''; if (!parent.$router) { - throw new Error('IonTabs requires an instance of either VueRouter or IonicVueRouter'); + throw new Error('IonTabsVue requires an instance of either VueRouter or IonicVueRouter'); } // Loop through all of the children in the default slot diff --git a/src/components/overlays.ts b/src/components/overlays.ts new file mode 100644 index 0000000..6db77ea --- /dev/null +++ b/src/components/overlays.ts @@ -0,0 +1,20 @@ +import { createOverlayComponent } from '../utils'; +import { + actionSheetController, + modalController, + popoverController +} from '../controllers'; + +export const IonModalVue = createOverlayComponent( + 'IonModal', + modalController +); + +export const IonActionSheetVue = createOverlayComponent< + HTMLIonActionSheetElement +>('IonActionSheet', actionSheetController); + +export const IonPopoverVue = createOverlayComponent( + 'IonPopover', + popoverController +); diff --git a/src/controllers/modal-controller.ts b/src/controllers/modal-controller.ts index bf823ba..7e76fc8 100644 --- a/src/controllers/modal-controller.ts +++ b/src/controllers/modal-controller.ts @@ -1,15 +1,21 @@ -import Vue from 'vue'; -import { ModalOptions, modalController as _modalController } from '@ionic/core'; +import { + ModalOptions, + OverlayController, + modalController as _modalController +} from '@ionic/core'; import { VueDelegate } from './vue-delegate'; -export const modalController = (delegate?: VueDelegate) => { - delegate = delegate || new VueDelegate(Vue); +export interface ModalController extends OverlayController { + create(options: ModalOptions): Promise; +} + +export const modalController = (): ModalController => { return { ..._modalController, create(options: ModalOptions) { return _modalController.create({ ...options, - delegate, + delegate: new VueDelegate() }); } }; diff --git a/src/controllers/popover-controller.ts b/src/controllers/popover-controller.ts index 83db272..bbdfaab 100644 --- a/src/controllers/popover-controller.ts +++ b/src/controllers/popover-controller.ts @@ -1,15 +1,16 @@ -import Vue from 'vue'; -import { PopoverOptions, popoverController as _popoverController } from '@ionic/core'; +import { + PopoverOptions, + popoverController as _popoverController +} from '@ionic/core'; import { VueDelegate } from './vue-delegate'; -export const popoverController = (delegate?: VueDelegate) => { - delegate = delegate || new VueDelegate(Vue); +export const popoverController = () => { return { ..._popoverController, create(options: PopoverOptions) { return _popoverController.create({ ...options, - delegate, + delegate: new VueDelegate() }); } }; diff --git a/src/controllers/vue-delegate.ts b/src/controllers/vue-delegate.ts index c0cc3d7..62be1c5 100644 --- a/src/controllers/vue-delegate.ts +++ b/src/controllers/vue-delegate.ts @@ -1,24 +1,36 @@ -import { VueConstructor } from 'vue'; -import { FrameworkDelegate, LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE, LIFECYCLE_WILL_UNLOAD } from '@ionic/core'; +import Vue, { VueConstructor } from 'vue'; +import { + FrameworkDelegate, + LIFECYCLE_DID_ENTER, + LIFECYCLE_DID_LEAVE, + LIFECYCLE_WILL_ENTER, + LIFECYCLE_WILL_LEAVE, + LIFECYCLE_WILL_UNLOAD +} from '@ionic/core'; import { EsModule, HTMLVueElement, WebpackFunction } from '../interfaces'; - // Handle creation of sync and async components -function createVueComponent(vue: VueConstructor, component: WebpackFunction | object | VueConstructor): Promise { - return Promise.resolve( - typeof component === 'function' && (component as WebpackFunction).cid === undefined - ? (component as WebpackFunction)().then((cmp: any) => vue.extend(isESModule(cmp) ? cmp.default : cmp)) - : vue.extend(component) - ); +async function createVueComponent( + component: WebpackFunction | object | VueConstructor +): Promise { + if ( + typeof component === 'function' && + (component as WebpackFunction).cid === undefined + ) { + const cmp = await (component as WebpackFunction)(); + return Vue.extend(isESModule(cmp) ? cmp.default : cmp); + } + return Vue.extend(component); } export class VueDelegate implements FrameworkDelegate { - constructor( - public vue: VueConstructor, - ) {} - // Attach the passed Vue component to DOM - attachViewToDom(parentElement: HTMLElement, component: HTMLElement | WebpackFunction | object | VueConstructor, opts?: object, classes?: string[]): Promise { + async attachViewToDom( + parentElement: HTMLElement, + component: HTMLElement | WebpackFunction | object | VueConstructor, + opts?: object, + classes?: string[] + ): Promise { // Handle HTML elements if (isElement(component)) { // Add any classes to the element @@ -30,22 +42,24 @@ export class VueDelegate implements FrameworkDelegate { return Promise.resolve(component as HTMLElement); } - // Get the Vue controller - return createVueComponent(this.vue, component).then((Component: VueConstructor) => { - const componentInstance = new Component(opts); - componentInstance.$mount(); + // Get the Vue constructor + const constructor = await createVueComponent(component); + const componentInstance = new constructor(opts); + componentInstance.$mount(); - // Add any classes to the Vue component's root element - addClasses(componentInstance.$el as HTMLElement, classes); + // Add any classes to the Vue component's root element + addClasses(componentInstance.$el as HTMLElement, classes); - // Append the Vue component to DOM - parentElement.appendChild(componentInstance.$el); - return componentInstance.$el as HTMLElement; - }); + // Append the Vue component to DOM + parentElement.appendChild(componentInstance.$el); + return componentInstance.$el as HTMLElement; } // Remove the earlier created Vue component from DOM - removeViewFromDom(_parentElement: HTMLElement, childElement: HTMLVueElement): Promise { + removeViewFromDom( + _parentElement: HTMLElement, + childElement: HTMLVueElement + ): Promise { // Destroy the Vue component instance if (childElement.__vue__) { childElement.__vue__.$destroy(); @@ -74,7 +88,8 @@ export function bindLifecycleEvents(instance: any, element: HTMLElement) { } // Check Symbol support -const hasSymbol = typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol'; +const hasSymbol = + typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol'; // Check if object is an ES module function isESModule(obj: EsModule) { diff --git a/src/index.ts b/src/index.ts index 01179ba..9092b39 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,7 +36,7 @@ export default { export { default as IonicVueRouter } from './router'; export * from './controllers'; -export * from './components/inputs'; +export * from './components'; // Icons that are used by internal components addIcons({ diff --git a/src/interfaces.ts b/src/interfaces.ts index cbd3d42..8c97cd6 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -8,6 +8,8 @@ declare module 'vue-router/types/router' { direction: RouterDirection; directionOverride: RouterDirection | null; transition: Promise; + saveScroll(el: HTMLElement): Promise; + restoreScroll(el: HTMLElement, key: string): Promise; canGoBack(): boolean; } } @@ -36,14 +38,17 @@ export interface IonicGlobal { asyncQueue?: boolean; } -export interface IonicWindow extends Window { - Ionic: IonicGlobal; - __zone_symbol__requestAnimationFrame?: (ts: FrameRequestCallback) => number; -} - export interface FrameworkDelegate { - attachViewToDom(parentElement: HTMLElement, component: HTMLElement | WebpackFunction | object | Vue, opts?: object, classes?: string[]): Promise; - removeViewFromDom(parentElement: HTMLElement, childElement: HTMLVueElement): Promise; + attachViewToDom( + parentElement: HTMLElement, + component: HTMLElement | WebpackFunction | object | Vue, + opts?: object, + classes?: string[] + ): Promise; + removeViewFromDom( + parentElement: HTMLElement, + childElement: HTMLVueElement + ): Promise; } export interface IonBackButton extends HTMLStencilElement { @@ -51,7 +56,11 @@ export interface IonBackButton extends HTMLStencilElement { } export interface IonRouterOutlet extends HTMLStencilElement { - commit(enterinEl: HTMLElement, leavingEl: HTMLElement | undefined, opts?: object | undefined): Promise; + commit( + enterinEl: HTMLElement, + leavingEl: HTMLElement | undefined, + opts?: object | undefined + ): Promise; } export interface ApiCache { diff --git a/src/ionic.ts b/src/ionic.ts index ff4fe61..0433ee7 100644 --- a/src/ionic.ts +++ b/src/ionic.ts @@ -1,91 +1,10 @@ -import VueImport, { PluginFunction, VueConstructor } from 'vue'; -import { IonicConfig, OverlayController } from '@ionic/core'; -import { - actionSheetController, - alertController, - loadingController, - menuController, - pickerController, - toastController -} from './controllers'; -import { modalController } from './controllers/modal-controller'; -import { popoverController } from './controllers/popover-controller'; -import { appInitialize } from './app-initialize'; -import { VueDelegate } from './controllers/vue-delegate'; -import IonTabs from './components/navigation/ion-tabs'; -import IonPage from './components/navigation/ion-page'; -import { createInputComponents } from './components/inputs'; - -interface Controllers { - actionSheetController: OverlayController; - alertController: OverlayController; - loadingController: OverlayController; - menuController: typeof menuController; - modalController: OverlayController; - popoverController: OverlayController; - toastController: OverlayController; - pickerController: OverlayController; -} - -function createApi(vueInstance: VueConstructor) { - const cache: Partial = {}; - const vueDelegate = new VueDelegate(vueInstance); - - return { - get actionSheetController() { - if (!cache.actionSheetController) { - cache.actionSheetController = actionSheetController; - } - return cache.actionSheetController; - }, - get alertController() { - if (!cache.alertController) { - cache.alertController = alertController; - } - return cache.alertController; - }, - get loadingController() { - if (!cache.loadingController) { - cache.loadingController = loadingController; - } - return cache.loadingController; - }, - get menuController() { - if (!cache.menuController) { - cache.menuController = menuController; - } - return cache.menuController; - }, - get modalController() { - if (!cache.modalController) { - cache.modalController = modalController(vueDelegate); - } - return cache.modalController; - }, - get popoverController() { - if (!cache.popoverController) { - cache.popoverController = popoverController(vueDelegate); - } - return cache.popoverController; - }, - get toastController() { - if (!cache.toastController) { - cache.toastController = toastController; - } - return cache.toastController; - }, - get pickerController() { - if (!cache.pickerController) { - cache.pickerController = pickerController; - } - return cache.pickerController; - } - }; -} +import VueImport, { PluginFunction } from 'vue'; +import { IonicConfig, setupConfig } from '@ionic/core'; +import { applyPolyfills, defineCustomElements } from '@ionic/core/loader'; let Vue: typeof VueImport; -export const install: PluginFunction = (_Vue, config) => { +export const install: PluginFunction = async (_Vue, config) => { if (Vue && _Vue === Vue) { if (process.env.NODE_ENV !== 'production') { console.error( @@ -96,10 +15,8 @@ export const install: PluginFunction = (_Vue, config) => { } Vue = _Vue; Vue.config.ignoredElements.push(/^ion-/); - Vue.component('IonTabs', IonTabs); - Vue.component('IonPage', IonPage); - createInputComponents(); - appInitialize(config); - createApi(Vue); + config && setupConfig(config); + await applyPolyfills(); + defineCustomElements(window); }; diff --git a/src/router.ts b/src/router.ts index 588b831..f2d7538 100644 --- a/src/router.ts +++ b/src/router.ts @@ -10,13 +10,18 @@ export default class Router extends VueRouter { directionOverride: RouterDirection | null; viewCount: number; prevRouteStack: Route[]; + cachedPrevRoute: Route; history: any; + scroll: Map; static installed: boolean; static install: PluginFunction; constructor(args: RouterArgs = {} as RouterArgs) { super(args); + // Set default scroll stack + this.scroll = new Map(); + // The direction user navigates in this.direction = args.direction || 'forward'; @@ -41,6 +46,10 @@ export default class Router extends VueRouter { }); } + get prevRoute(): Route | undefined { + return this.prevRouteStack[this.prevRouteStack.length - 1]; + } + extendTransitionConfirmation() { this.history._confirmTransition = this.history.confirmTransition; this.history.confirmTransition = async (...opts: any) => { @@ -82,13 +91,13 @@ export default class Router extends VueRouter { } guessDirection(nextRoute: Route): RouterDirection { - if (this.prevRouteStack.length !== 0) { - const prevRoute: Route = this.prevRouteStack[this.prevRouteStack.length - 1]; + this.cachedPrevRoute = this.history.current; + if (this.prevRoute) { // Last route is the same as the next one - go back // If we're going to / reset the stack otherwise pop a route - if (prevRoute.fullPath === nextRoute.fullPath) { - if (prevRoute.fullPath.length === 1) { + if (this.prevRoute.fullPath === nextRoute.fullPath) { + if (this.prevRoute.fullPath.length === 1) { this.prevRouteStack = []; } else { this.prevRouteStack.pop(); @@ -101,8 +110,27 @@ export default class Router extends VueRouter { if (this.history.current.fullPath !== nextRoute.fullPath) { this.prevRouteStack.push(this.history.current); } + return 'forward'; } + + async saveScroll(el: HTMLElement): Promise { + const ionContent = el.querySelector('ion-content'); + const scrollElement = ionContent && (await ionContent.getScrollElement()); + + if (scrollElement) { + this.scroll.set(this.cachedPrevRoute.fullPath, { + top: scrollElement?.scrollTop || 0, + left: scrollElement?.scrollLeft || 0, + }); + } + } + + async restoreScroll(el: HTMLElement, key: string): Promise { + const ionContent = el.querySelector('ion-content'); + const scrollElement = ionContent && (await ionContent.getScrollElement()) || undefined; + scrollElement?.scrollTo(this.scroll.get(key) || { top: 0, left: 0 }); + } } Router.install = (Vue) => { diff --git a/src/utils/createInputComponent.ts b/src/utils/createInputComponent.ts new file mode 100644 index 0000000..a1c8132 --- /dev/null +++ b/src/utils/createInputComponent.ts @@ -0,0 +1,74 @@ +import Vue, { CreateElement, RenderContext } from 'vue'; + +interface EventHandler { + [key: string]: (e: Event) => void; +} + +type Callback = (value: Event | string) => void; + +// Events to register handlers for +const events: string[] = [ + 'ionChange', + 'ionInput', + 'ionBlur', + 'ionFocus', + 'ionCancel', + 'ionSelect' +]; + +export function createInputComponent( + name: string, + coreTag: string, + modelEvent = 'ionChange', + valueProperty = 'value' +) { + return Vue.extend({ + name, + functional: true, + model: { + event: modelEvent, + prop: valueProperty + }, + render(h: CreateElement, { data, listeners, slots }: RenderContext) { + return h( + coreTag, + { + ...data, + on: buildEventHandlers(listeners, modelEvent, valueProperty) + }, + slots().default + ); + } + }); +} + +function buildEventHandlers( + listeners: RenderContext['listeners'], + modelEvent: string, + valueProperty: string +) { + const handlers: EventHandler = {}; + + // Loop through all the events + events.map((eventName: string) => { + if (!listeners[eventName]) { + return; + } + + // Normalize listeners coming from context as Function | Function[] + const callbacks: Callback[] = Array.isArray(listeners[eventName]) + ? (listeners[eventName] as Callback[]) + : [listeners[eventName] as Callback]; + + // Assign handlers + handlers[eventName] = (e: Event) => { + callbacks.map(f => { + if (e) { + f(modelEvent === eventName ? (e.target as any)[valueProperty] : e); + } + }); + }; + }); + + return handlers; +} diff --git a/src/utils/createOverlayComponent.ts b/src/utils/createOverlayComponent.ts new file mode 100644 index 0000000..7b9640d --- /dev/null +++ b/src/utils/createOverlayComponent.ts @@ -0,0 +1,100 @@ +import Vue, { CreateElement, VueConstructor } from 'vue'; + +export interface OverlayElement extends HTMLElement { + present: () => Promise; + dismiss: (data?: any, role?: string | undefined) => Promise; +} + +export interface Data { + overlay: T | null; +} + +export interface Methods { + present: () => Promise; +} + +export interface Props { + isOpen: boolean; +} + +export function createOverlayComponent( + name: string, + controller: { create: (opts: any) => Promise } +): VueConstructor & Methods & Props & Vue> { + const coreTag = name.charAt(0).toLowerCase() + name.slice(1); + const eventHandlers = Object.entries({ + [`${coreTag}WillPresent`]: 'onWillPresent', + [`${coreTag}DidPresent`]: 'onDidPresent', + [`${coreTag}WillDismiss`]: 'onWillDismiss', + [`${coreTag}DidDismiss`]: 'onDidDismiss', + }); + + return Vue.extend, Methods, {}, Props>({ + name: `${name}Vue`, + data() { + return { + overlay: null, + }; + }, + props: { + isOpen: { + type: Boolean, + required: true, + }, + }, + watch: { + async isOpen(newVal: boolean) { + console.log(newVal); + if (newVal) { + if (this.overlay) { + await this.overlay.present(); + } else { + await this.present(); + } + } else { + await this.overlay?.dismiss(); + this.overlay = null; + } + } + }, + async mounted() { + console.log(this.isOpen); + this.isOpen && await this.present(); + }, + async beforeDestroy() { + console.log('bye'); + await this.overlay?.dismiss(); + }, + render(h: CreateElement) { + for (const [eventName, handler] of eventHandlers) { + console.log(eventName, handler); + if (this.$listeners[handler]) { + this.overlay?.addEventListener(eventName, (e: Event) => { + const handlers = this.$listeners[handler]; + if (Array.isArray(handlers)) { + handlers.map(f => f(e)); + return; + } + handlers(e); + }); + } + } + return h('div', [h('div', { ref: 'overlay' }, this.overlay ? this.$slots.default : null)]); + }, + methods: { + async present() { + + // const children = this.$slots.default; + this.overlay = await controller.create({ + ...this.$attrs, + component: this.$refs.overlay, + componentProps: { parent: this }, + }); + + console.log(this.overlay); + + await this.overlay.present(); + } + } + }); +} diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..4fa8db2 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './createOverlayComponent'; +export * from './createInputComponent'; diff --git a/types/app-initialize.d.ts b/types/app-initialize.d.ts deleted file mode 100644 index e13c240..0000000 --- a/types/app-initialize.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { IonicConfig } from '@ionic/core'; -export declare function appInitialize(config?: IonicConfig): Promise; -//# sourceMappingURL=app-initialize.d.ts.map \ No newline at end of file diff --git a/types/components/inputs.d.ts b/types/components/inputs.d.ts deleted file mode 100644 index cb9e62d..0000000 --- a/types/components/inputs.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export declare function createInputComponents(): void; -//# sourceMappingURL=inputs.d.ts.map \ No newline at end of file diff --git a/types/components/ion-vue-router.d.ts b/types/components/ion-vue-router.d.ts deleted file mode 100644 index 37cdec2..0000000 --- a/types/components/ion-vue-router.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { CreateElement, RenderContext, VNode } from 'vue'; -interface KeepAliveProps { - include?: string | string[] | RegExp; - exclude?: string | string[] | RegExp; - max?: number; -} -declare const _default: { - name: string; - functional: boolean; - props: { - name: { - default: string; - type: StringConstructor; - }; - animated: { - default: boolean; - type: BooleanConstructor; - }; - keepAlive: { - type: (StringConstructor | (() => KeepAliveProps))[]; - }; - }; - render(h: CreateElement, { parent, props, data, children }: RenderContext>): VNode; -}; -export default _default; -//# sourceMappingURL=ion-vue-router.d.ts.map \ No newline at end of file diff --git a/types/components/navigation/ion-page.d.ts b/types/components/navigation/ion-page.d.ts deleted file mode 100644 index 3afb446..0000000 --- a/types/components/navigation/ion-page.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { CreateElement, RenderContext } from 'vue'; -declare const _default: { - name: string; - functional: boolean; - render(h: CreateElement, { children }: RenderContext>): import("vue").VNode; -}; -export default _default; -//# sourceMappingURL=ion-page.d.ts.map \ No newline at end of file diff --git a/types/components/navigation/ion-tabs.d.ts b/types/components/navigation/ion-tabs.d.ts deleted file mode 100644 index e99dec9..0000000 --- a/types/components/navigation/ion-tabs.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { CreateElement, RenderContext, VNode } from 'vue'; -declare const _default: { - name: string; - functional: boolean; - render(h: CreateElement, { parent, data, slots, listeners }: RenderContext>): VNode; -}; -export default _default; -//# sourceMappingURL=ion-tabs.d.ts.map \ No newline at end of file diff --git a/types/controllers/index.d.ts b/types/controllers/index.d.ts deleted file mode 100644 index 97feb28..0000000 --- a/types/controllers/index.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -export { actionSheetController, alertController, loadingController, menuController, toastController, pickerController, } from '@ionic/core'; -export declare const modalController: { - create(options: import("@ionic/core").ModalOptions): Promise; - dismiss(data?: any, role?: string | undefined, id?: string | undefined): Promise; - getTop(): Promise; -}; -export declare const popoverController: { - create(options: import("@ionic/core").PopoverOptions): Promise; - dismiss(data?: any, role?: string | undefined, id?: string | undefined): Promise; - getTop(): Promise; -}; -//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/types/controllers/modal-controller.d.ts b/types/controllers/modal-controller.d.ts deleted file mode 100644 index 8e3312c..0000000 --- a/types/controllers/modal-controller.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { ModalOptions } from '@ionic/core'; -import { VueDelegate } from './vue-delegate'; -export declare const modalController: (delegate?: VueDelegate | undefined) => { - create(options: ModalOptions): Promise; - dismiss(data?: any, role?: string | undefined, id?: string | undefined): Promise; - getTop(): Promise; -}; -//# sourceMappingURL=modal-controller.d.ts.map \ No newline at end of file diff --git a/types/controllers/popover-controller.d.ts b/types/controllers/popover-controller.d.ts deleted file mode 100644 index 590ee0a..0000000 --- a/types/controllers/popover-controller.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { PopoverOptions } from '@ionic/core'; -import { VueDelegate } from './vue-delegate'; -export declare const popoverController: (delegate?: VueDelegate | undefined) => { - create(options: PopoverOptions): Promise; - dismiss(data?: any, role?: string | undefined, id?: string | undefined): Promise; - getTop(): Promise; -}; -//# sourceMappingURL=popover-controller.d.ts.map \ No newline at end of file diff --git a/types/controllers/vue-delegate.d.ts b/types/controllers/vue-delegate.d.ts deleted file mode 100644 index da34078..0000000 --- a/types/controllers/vue-delegate.d.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { VueConstructor } from 'vue'; -import { FrameworkDelegate } from '@ionic/core'; -import { HTMLVueElement, WebpackFunction } from '../interfaces'; -export declare class VueDelegate implements FrameworkDelegate { - vue: VueConstructor; - constructor(vue: VueConstructor); - attachViewToDom(parentElement: HTMLElement, component: HTMLElement | WebpackFunction | object | VueConstructor, opts?: object, classes?: string[]): Promise; - removeViewFromDom(_parentElement: HTMLElement, childElement: HTMLVueElement): Promise; -} -export declare function bindLifecycleEvents(instance: any, element: HTMLElement): void; -//# sourceMappingURL=vue-delegate.d.ts.map \ No newline at end of file diff --git a/types/index.d.ts b/types/index.d.ts deleted file mode 100644 index 765085a..0000000 --- a/types/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -export { createAnimation, createGesture, AlertButton, AlertInput, Gesture, GestureConfig, GestureDetail, mdTransitionAnimation, iosTransitionAnimation, setupConfig } from '@ionic/core'; -declare const _default: { - install: import("vue").PluginFunction; - version: string; -}; -export default _default; -export { default as IonicVueRouter } from './router'; -export * from './controllers'; -export * from './components/inputs'; -//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/types/interfaces.d.ts b/types/interfaces.d.ts deleted file mode 100644 index 24ae769..0000000 --- a/types/interfaces.d.ts +++ /dev/null @@ -1,74 +0,0 @@ -import Vue from 'vue'; -import VueRouter from 'vue-router'; -import { HTMLStencilElement, IonicConfig, RouterDirection } from '@ionic/core'; -import { RouterOptions } from 'vue-router/types/router'; -declare module 'vue-router/types/router' { - interface VueRouter { - direction: RouterDirection; - directionOverride: RouterDirection | null; - transition: Promise; - canGoBack(): boolean; - } -} -export interface HTMLVueElement extends HTMLElement { - __vue__: Vue; -} -export interface VueWindow extends Window { - Vue: typeof Vue; - VueRouter: typeof VueRouter; - disableIonicTransitions: boolean; -} -export interface WebpackFunction extends Function { - cid: number; -} -export interface EsModule extends Object { - __esModule?: boolean; - [Symbol.toStringTag]: string; -} -export interface IonicGlobal { - config?: IonicConfig; - asyncQueue?: boolean; -} -export interface IonicWindow extends Window { - Ionic: IonicGlobal; - __zone_symbol__requestAnimationFrame?: (ts: FrameRequestCallback) => number; -} -export interface FrameworkDelegate { - attachViewToDom(parentElement: HTMLElement, component: HTMLElement | WebpackFunction | object | Vue, opts?: object, classes?: string[]): Promise; - removeViewFromDom(parentElement: HTMLElement, childElement: HTMLVueElement): Promise; -} -export interface IonBackButton extends HTMLStencilElement { - defaultHref?: string; -} -export interface IonRouterOutlet extends HTMLStencilElement { - commit(enterinEl: HTMLElement, leavingEl: HTMLElement | undefined, opts?: object | undefined): Promise; -} -export interface ApiCache { - [key: string]: any; -} -export interface RouterArgs extends RouterOptions { - direction?: RouterDirection; - viewCount?: number; -} -export interface ProxyControllerInterface { - create(opts: object): Promise; - dismiss(): Promise; - getTop(): Promise; -} -export interface ProxyDelegateOptions extends Object { - [key: string]: any; - delegate?: FrameworkDelegate; -} -export interface ProxyMenuControllerInterface { - open(menuId?: string): Promise; - close(menuId?: string): Promise; - toggle(menuId?: string): Promise; - enable(shouldEnable: boolean, menuId?: string): Promise; - swipeEnable(shouldEnable: boolean, menuId?: string): Promise; - isOpen(menuId?: string): Promise; - isEnabled(menuId?: string): Promise; - get(menuId?: string): Promise; - getOpen(): Promise; - getMenus(): Promise; -} -//# sourceMappingURL=interfaces.d.ts.map \ No newline at end of file diff --git a/types/ionic.d.ts b/types/ionic.d.ts deleted file mode 100644 index 8ce9959..0000000 --- a/types/ionic.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { PluginFunction } from 'vue'; -import { IonicConfig } from '@ionic/core'; -export declare const install: PluginFunction; -//# sourceMappingURL=ionic.d.ts.map \ No newline at end of file diff --git a/types/router.d.ts b/types/router.d.ts deleted file mode 100644 index 66330a2..0000000 --- a/types/router.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -import VueRouter, { Route } from 'vue-router'; -import { PluginFunction } from 'vue'; -import { RouterArgs } from './interfaces'; -import { RouterDirection } from '@ionic/core'; -export default class Router extends VueRouter { - direction: RouterDirection; - directionOverride: RouterDirection | null; - viewCount: number; - prevRouteStack: Route[]; - history: any; - static installed: boolean; - static install: PluginFunction; - constructor(args?: RouterArgs); - extendTransitionConfirmation(): void; - extendHistory(): void; - canGoBack(): boolean; - guessDirection(nextRoute: Route): RouterDirection; -} -//# sourceMappingURL=router.d.ts.map \ No newline at end of file