Skip to content

Commit eedbe71

Browse files
Rework framework delegation (#27)
* Rework framework delegation * Fix and improve tests * Streamline exports * Fix circular dependency and imporove delegation * Improve router init * Reuse Delegate class instance * Store Delegate instance as a static property * Allow Vue to cleanup, don't remove from DOM * Enable lazy loading with Ionic. Update to latest Ionic beta. Update tests
1 parent 2d7923b commit eedbe71

11 files changed

+166
-132
lines changed

build/rollup.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ function baseConfig() {
4141
'vue',
4242
'vue-router',
4343
'@ionic/core/dist/esm',
44-
'@ionic/core/css/ionic.css',
44+
'@ionic/core/css/ionic.bundle.css',
4545
'@ionic/core/dist/ionic/svg',
4646
'ionicons/dist/collection/icon/icon.css',
4747
],

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
"vue": "^2.5.16",
6666
"vue-template-compiler": "^2.5.16",
6767
"vue-router": "^3.0.1",
68-
"@ionic/core": "4.0.0-beta.4"
68+
"@ionic/core": "4.0.0-beta.7"
6969
},
7070
"jestSonar": {
7171
"reportPath": "reports",

src/api-utils.js

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,15 @@
1-
import { Delegate } from './framework-delegate'
2-
31
// A proxy method that initializes the controller and calls requested method
42
export function proxyMethod(tag, method, ...opts) {
53
return initController(tag).then(ctrl => ctrl[method].apply(ctrl, opts))
64
}
75

86
// Initialize an Ionic controller and append it to DOM
97
export function initController(tag) {
10-
const element = getOrAppendElement(tag)
11-
12-
// Set the framework-specific implementations for Ionic's internals
13-
element.delegate = Delegate
14-
15-
// Return a Promise
16-
return element.componentOnReady()
17-
}
18-
19-
// Return existing Element (tag) or create a new one
20-
function getOrAppendElement(tag) {
218
let element = document.querySelector(tag)
229

2310
if (element) {
24-
return element
11+
return element.componentOnReady()
2512
}
2613

27-
return document.body.appendChild(document.createElement(tag))
14+
return document.body.appendChild(document.createElement(tag)).componentOnReady()
2815
}

src/api.js

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,67 @@
1+
import Delegate from './framework-delegate'
12
import ProxyController from './proxy-controller'
23
import ProxyMenuController from './proxy-menu-controller'
4+
import ProxyDelegateController from './proxy-delegate-controller'
5+
6+
let _Vue, _Delegate
37

48
export default class Api {
59
// Create or return a ActionSheetController instance
610
get actionSheetController() {
7-
return getOrCreateController('_actionSheetController', 'ion-action-sheet-controller')
11+
return getOrCreateController('ion-action-sheet-controller')
812
}
913

1014
// Create or return an AlertController instance
1115
get alertController() {
12-
return getOrCreateController('_alertController', 'ion-alert-controller')
16+
return getOrCreateController('ion-alert-controller')
1317
}
1418

1519
// Create or return a LoadingController instance
1620
get loadingController() {
17-
return getOrCreateController('_loadingController', 'ion-loading-controller')
21+
return getOrCreateController('ion-loading-controller')
1822
}
1923

2024
// Create or return a MenuController instance
2125
get menuController() {
22-
return getOrCreateController('_menuController', 'ion-menu-controller', ProxyMenuController)
26+
return getOrCreateMenuController('ion-menu-controller')
2327
}
2428

2529
// Create or return a ModalController instance
2630
get modalController() {
27-
return getOrCreateController('_modalController', 'ion-modal-controller')
31+
return getOrCreateDelegatedController('ion-modal-controller')
2832
}
2933

3034
// Create or return a PopoverController instance
3135
get popoverController() {
32-
return getOrCreateController('_popoverController', 'ion-popover-controller')
36+
return getOrCreateDelegatedController('ion-popover-controller')
3337
}
3438

3539
// Create or return a ToastController instance
3640
get toastController() {
37-
return getOrCreateController('_toastController', 'ion-toast-controller')
41+
return getOrCreateController('ion-toast-controller')
3842
}
3943
}
4044

4145
// Cached controllers
42-
Api._actionSheetController = null
43-
Api._alertController = null
44-
Api._loadingController = null
45-
Api._menuController = null
46-
Api._modalController = null
47-
Api._popoverController = null
48-
Api._toastController = null
46+
Api.cache = {
47+
'ion-action-sheet-controller': null,
48+
'ion-alert-controller': null,
49+
'ion-loading-controller': null,
50+
'ion-menu-controller': null,
51+
'ion-modal-controller': null,
52+
'ion-popover-controller': null,
53+
'ion-toast-controller': null,
54+
}
4955

5056
Api.install = function(Vue) {
5157
// If installed - skip
52-
if (Api.install.installed) {
58+
if (Api.install.installed && _Vue === Vue) {
5359
return
5460
}
5561

62+
_Vue = Vue
63+
_Delegate = new Delegate(Vue)
64+
5665
Api.install.installed = true
5766

5867
// Ignore Ionic custom elements
@@ -66,11 +75,29 @@ Api.install = function(Vue) {
6675
})
6776
}
6877

69-
// Get existing controller instance or initialize a new one
70-
function getOrCreateController(cache, tag, proxy = ProxyController) {
71-
if (!Api[cache]) {
72-
Api[cache] = new proxy(tag)
78+
// Get existing Base controller instance or initialize a new one
79+
function getOrCreateController(tag) {
80+
if (!Api.cache[tag]) {
81+
Api.cache[tag] = new ProxyController(tag)
82+
}
83+
84+
return Api.cache[tag]
85+
}
86+
87+
// Get existing Menu controller instance or initialize a new one
88+
function getOrCreateMenuController(tag) {
89+
if (!Api.cache[tag]) {
90+
Api.cache[tag] = new ProxyMenuController(tag)
91+
}
92+
93+
return Api.cache[tag]
94+
}
95+
96+
// Get existing Delegated controller instance or initialize a new one
97+
function getOrCreateDelegatedController(tag) {
98+
if (!Api.cache[tag]) {
99+
Api.cache[tag] = new ProxyDelegateController(tag, _Delegate)
73100
}
74101

75-
return Api[cache]
102+
return Api.cache[tag]
76103
}

src/framework-delegate.js

Lines changed: 41 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,56 @@
1-
import Vue from 'vue'
2-
3-
let globalVue = null
4-
5-
// Detect environment (browser, module, etc.)
6-
if (typeof window !== 'undefined' && window.Vue !== undefined) {
7-
globalVue = window.Vue
8-
} else if (typeof global !== 'undefined') {
9-
globalVue = global.Vue
10-
}
11-
12-
if (!globalVue) {
13-
globalVue = Vue
14-
globalVue.config.productionTip = false
15-
}
16-
17-
// Attach the passed Vue component to DOM
18-
export function attachViewToDom(parentElement, vueComponent, propsData, classes) {
19-
// Create an appropriate wrapper for the component
20-
const wrapper = document.createElement(shouldWrapInIonPage(parentElement) ? 'ion-page' : 'div')
1+
export default class Delegate {
2+
constructor(Vue) {
3+
this.Vue = Vue
4+
}
215

22-
parentElement.appendChild(wrapper)
6+
// Attach the passed Vue component to DOM
7+
attachViewToDom(parentElement, component, opts, classes) {
8+
// Get the Vue controller
9+
return this.vueController(component).then(controller => {
10+
const vueComponent = this.vueComponent(controller, opts)
2311

24-
// Create a Vue component constructor
25-
const vueElement = globalVue.extend(vueComponent)
12+
// Add any classes to the Vue component's root element
13+
addClasses(vueComponent.$el, classes)
2614

27-
// Create a new instance of the Vue component
28-
const page = new vueElement({ propsData }).$mount(wrapper)
29-
30-
// Add any classes the Vue component's root element
31-
if (classes) {
32-
for (const cls of classes) {
33-
page.$el.classList.add(cls)
34-
}
15+
// Append the Vue component to DOM
16+
parentElement.appendChild(vueComponent.$el)
17+
return vueComponent.$el
18+
})
3519
}
3620

37-
// Resolve the Vue component element
38-
return Promise.resolve(page.$el)
39-
}
21+
// Remove the earlier created Vue component from DOM
22+
removeViewFromDom(parentElement, childElement) {
23+
// Destroy the Vue component instance
24+
if (childElement.__vue__) {
25+
childElement.__vue__.$destroy()
26+
}
4027

41-
// Remove the earlier created Vue component from DOM
42-
export function removeViewFromDom(parentElement, childElement) {
43-
// Destroy the Vue component instance
44-
if (childElement.hasOwnProperty('__vue__')) {
45-
childElement.__vue__.$destroy()
28+
return Promise.resolve()
4629
}
4730

48-
// Remove from DOM
49-
parentElement.removeChild(childElement)
50-
51-
return Promise.resolve()
52-
}
31+
// Handle creation of sync and async components
32+
vueController(component) {
33+
return Promise.resolve(
34+
typeof component === 'function' && component.cid === undefined
35+
? component().then(c => this.Vue.extend(isESModule(c) ? c.default : c))
36+
: this.Vue.extend(component)
37+
)
38+
}
5339

54-
const Delegate = {
55-
attachViewToDom,
56-
removeViewFromDom,
40+
// Create a new instance of the Vue component
41+
vueComponent(controller, opts) {
42+
return new controller(opts).$mount()
43+
}
5744
}
5845

59-
export { Delegate }
46+
const hasSymbol = typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol'
6047

61-
// Detect wrapper to be used
62-
function shouldWrapInIonPage(element) {
63-
return isElementModal(element) || isElementNav(element)
48+
function isESModule(obj) {
49+
return obj.__esModule || (hasSymbol && obj[Symbol.toStringTag] === 'Module')
6450
}
6551

66-
// Check if element is ION-NAV
67-
function isElementNav(element) {
68-
return element.tagName.toUpperCase() === 'ION-NAV'
69-
}
70-
71-
// Check if element has modal-wrapper class
72-
function isElementModal(element) {
73-
return element.classList.contains('modal-wrapper')
52+
function addClasses(element, classes = []) {
53+
for (const cls of classes) {
54+
element.classList.add(cls)
55+
}
7456
}

src/ionic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import '@ionic/core/css/ionic.css'
1+
import '@ionic/core/css/ionic.bundle.css'
22
import 'ionicons/dist/collection/icon/icon.css'
33
import '@ionic/core/dist/ionic/svg'
44
import { defineCustomElements } from '@ionic/core/dist/esm'

src/proxy-controller.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export default class ProxyController {
66
this.tag = tag
77
}
88

9-
create(opts) {
9+
create(opts = {}) {
1010
return apiUtils.proxyMethod(this.tag, 'create', opts)
1111
}
1212

src/proxy-delegate-controller.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import ProxyController from './proxy-controller'
2+
3+
// A proxy class that allows early access to controller methods
4+
export default class ProxyDelegateController extends ProxyController {
5+
constructor(tag, delegate) {
6+
super(tag)
7+
8+
if (!ProxyDelegateController.delegate) {
9+
ProxyDelegateController.delegate = delegate
10+
}
11+
}
12+
13+
create(opts = {}) {
14+
opts.delegate = ProxyDelegateController.delegate
15+
return super.create(opts)
16+
}
17+
}

src/router.js

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,13 @@ import VueRouter from 'vue-router'
22
import IonVueRouter from './components/ion-vue-router.vue'
33
import IonVueRouterTransitionless from './components/ion-vue-router-transitionless.vue'
44

5-
let globalVue = null
6-
let globalVueRouter = null
7-
let globalDisableIonicTransitions = false
5+
const inBrowser = typeof window !== 'undefined'
86

97
// Detect environment (browser, module, etc.)
10-
if (typeof window !== 'undefined' && window.Vue !== undefined) {
11-
globalVue = window.Vue
12-
globalVueRouter = window.VueRouter
13-
globalDisableIonicTransitions = window.disableIonicTransitions
14-
} else if (typeof global !== 'undefined') {
15-
globalVue = global.Vue
16-
globalVueRouter = global.VueRouter
17-
globalDisableIonicTransitions = global.disableIonicTransitions
18-
}
19-
20-
if (!globalVueRouter) {
21-
globalVueRouter = VueRouter
22-
}
8+
const _VueRouter = inBrowser && window.VueRouter ? window.VueRouter : VueRouter
239

2410
// Extend the official VueRouter
25-
export default class Router extends globalVueRouter {
11+
export default class Router extends _VueRouter {
2612
constructor(...args) {
2713
super(...args)
2814

@@ -97,17 +83,14 @@ Router.install = function(Vue, { disableIonicTransitions } = {}) {
9783
Router.install.installed = true
9884

9985
// Install the official VueRouter
100-
globalVueRouter.install(Vue)
86+
_VueRouter.install(Vue)
10187

10288
// Register the IonVueRouter component globally
10389
// either with default Ionic transitions turned on or off
104-
Vue.component(
105-
'IonVueRouter',
106-
!disableIonicTransitions ? IonVueRouter : IonVueRouterTransitionless
107-
)
90+
Vue.component('IonVueRouter', disableIonicTransitions ? IonVueRouterTransitionless : IonVueRouter)
10891
}
10992

11093
// Auto-install when Vue is found (i.e. in browser via <script> tag)
111-
if (globalVue) {
112-
globalVue.use(Router, { disableIonicTransitions: globalDisableIonicTransitions })
94+
if (inBrowser && window.Vue) {
95+
window.Vue.use(Router, { disableIonicTransitions: window.disableIonicTransitions })
11396
}

test/api.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe('API', () => {
3939
const app = new Vue()
4040

4141
expect(typeof app.$ionic).toBe('object')
42-
expect(API.install()).toBeFalsy()
42+
expect(API.install(Vue)).toBeFalsy()
4343
})
4444

4545
it('Creates action sheet controller', () => {

0 commit comments

Comments
 (0)