|
| 1 | +import omit from 'lodash.omit' |
| 2 | +// Components |
| 3 | +import CMeteorData from './components/MeteorData' |
| 4 | +import CMeteorSub from './components/MeteorSub' |
| 5 | + |
| 6 | +function defaultSubscription (...args) { |
| 7 | + return Meteor.subscribe(...args) |
| 8 | +} |
| 9 | + |
| 10 | +function hasProperty (holder, key) { |
| 11 | + return typeof holder !== 'undefined' && holder.hasOwnProperty(key) |
| 12 | +} |
| 13 | + |
| 14 | +const noop = () => {} |
| 15 | + |
| 16 | +let set |
| 17 | + |
| 18 | +export default { |
| 19 | + install (Vue, options) { |
| 20 | + const isServer = Vue.prototype.$isServer |
| 21 | + const vueVersion = Vue.version.substr(0, Vue.version.indexOf('.')) |
| 22 | + |
| 23 | + Vue.config.meteor = { |
| 24 | + subscribe: defaultSubscription, |
| 25 | + freeze: false, |
| 26 | + } |
| 27 | + |
| 28 | + set = Vue.set |
| 29 | + |
| 30 | + for (const k in options) { |
| 31 | + Vue.config.meteor[k] = options[k] |
| 32 | + } |
| 33 | + |
| 34 | + const merge = Vue.config.optionMergeStrategies.methods |
| 35 | + Vue.config.optionMergeStrategies.meteor = function (toVal, fromVal, vm) { |
| 36 | + if (!toVal) return fromVal |
| 37 | + if (!fromVal) return toVal |
| 38 | + |
| 39 | + const toData = Object.assign({}, omit(toVal, [ |
| 40 | + 'subscribe', |
| 41 | + 'data', |
| 42 | + ]), toVal.data) |
| 43 | + const fromData = Object.assign({}, omit(fromVal, [ |
| 44 | + 'subscribe', |
| 45 | + 'data', |
| 46 | + ]), fromVal.data) |
| 47 | + |
| 48 | + return Object.assign({ |
| 49 | + subscribe: merge(toVal.subscribe, fromVal.subscribe), |
| 50 | + }, merge(toData, fromData)) |
| 51 | + } |
| 52 | + |
| 53 | + function prepare () { |
| 54 | + this._trackerHandles = [] |
| 55 | + this._subsAutorun = {} |
| 56 | + this._subs = {} |
| 57 | + |
| 58 | + Object.defineProperty(this, '$subReady', { |
| 59 | + get: () => this.$data.$meteor.subs, |
| 60 | + enumerable: true, |
| 61 | + configurable: true, |
| 62 | + }) |
| 63 | + } |
| 64 | + |
| 65 | + function launch () { |
| 66 | + this._meteorActive = true |
| 67 | + |
| 68 | + let meteor = this.$options.meteor |
| 69 | + |
| 70 | + if (meteor) { |
| 71 | + let ssr = true |
| 72 | + if (typeof meteor.$ssr !== 'undefined') { |
| 73 | + ssr = meteor.$ssr |
| 74 | + } |
| 75 | + |
| 76 | + if (!isServer || ssr) { |
| 77 | + // Subscriptions |
| 78 | + if (meteor.subscribe || meteor.$subscribe) { |
| 79 | + const subscribeOptions = Object.assign({}, meteor.subscribe, meteor.$subscribe) |
| 80 | + for (let key in subscribeOptions) { |
| 81 | + this.$addReactiveSub(key, subscribeOptions[key]) |
| 82 | + } |
| 83 | + } |
| 84 | + |
| 85 | + const data = Object.assign({}, omit(meteor, [ |
| 86 | + 'subscribe', |
| 87 | + 'data', |
| 88 | + ]), meteor.data) |
| 89 | + |
| 90 | + // Reactive data |
| 91 | + if (data) { |
| 92 | + for (let key in data) { |
| 93 | + if (key.charAt(0) !== '$') { |
| 94 | + const options = data[key] |
| 95 | + let func |
| 96 | + if (typeof options === 'function') { |
| 97 | + func = options.bind(this) |
| 98 | + } else { |
| 99 | + throw Error(`Meteor data '${key}': You must provide a function which returns the result.`) |
| 100 | + } |
| 101 | + |
| 102 | + this.$addMeteorData(key, func) |
| 103 | + } |
| 104 | + } |
| 105 | + } |
| 106 | + } |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + Vue.mixin({ |
| 111 | + data () { |
| 112 | + return { |
| 113 | + $meteor: { |
| 114 | + data: {}, |
| 115 | + subs: {}, |
| 116 | + }, |
| 117 | + } |
| 118 | + }, |
| 119 | + |
| 120 | + ...vueVersion === '1' ? { |
| 121 | + init: prepare, |
| 122 | + } : {}, |
| 123 | + |
| 124 | + ...vueVersion === '2' ? { |
| 125 | + beforeCreate: prepare, |
| 126 | + } : {}, |
| 127 | + |
| 128 | + created () { |
| 129 | + if (this.$options.meteor && !this.$options.meteor.$lazy) { |
| 130 | + launch.call(this) |
| 131 | + } |
| 132 | + }, |
| 133 | + |
| 134 | + destroyed: function () { |
| 135 | + this.$stopMeteor() |
| 136 | + }, |
| 137 | + |
| 138 | + methods: { |
| 139 | + $subscribe (...args) { |
| 140 | + if (args.length > 0) { |
| 141 | + const key = args[0] |
| 142 | + const oldSub = this._subs[key] |
| 143 | + let handle = Vue.config.meteor.subscribe.apply(this, args) |
| 144 | + this._trackerHandles.push(handle) |
| 145 | + this._subs[key] = handle |
| 146 | + |
| 147 | + // Readiness |
| 148 | + if (typeof handle.ready === 'function') { |
| 149 | + set(this.$data.$meteor.subs, key, false) |
| 150 | + if (this._subsAutorun[key]) { |
| 151 | + this._subsAutorun[key].stop() |
| 152 | + } |
| 153 | + const autorun = this.$autorun(() => { |
| 154 | + const ready = handle.ready() |
| 155 | + set(this.$data.$meteor.subs, key, ready) |
| 156 | + // Wait for the new subscription to be ready before stoping the old one |
| 157 | + if (ready && oldSub) { |
| 158 | + this.$stopHandle(oldSub) |
| 159 | + } |
| 160 | + }) |
| 161 | + this._subsAutorun[key] = autorun |
| 162 | + } |
| 163 | + |
| 164 | + return handle |
| 165 | + // } |
| 166 | + } else { |
| 167 | + throw new Error('You must provide the publication name to $subscribe.') |
| 168 | + } |
| 169 | + }, |
| 170 | + |
| 171 | + $autorun (reactiveFunction) { |
| 172 | + let handle = Tracker.autorun(reactiveFunction) |
| 173 | + this._trackerHandles.push(handle) |
| 174 | + return handle |
| 175 | + }, |
| 176 | + |
| 177 | + $stopHandle (handle) { |
| 178 | + handle.stop() |
| 179 | + let index = this._trackerHandles.indexOf(handle) |
| 180 | + if (index !== -1) { |
| 181 | + this._trackerHandles.splice(index, 1) |
| 182 | + } |
| 183 | + }, |
| 184 | + |
| 185 | + $startMeteor () { |
| 186 | + if (!this._meteorActive) { |
| 187 | + launch.call(this) |
| 188 | + } |
| 189 | + }, |
| 190 | + |
| 191 | + $stopMeteor () { |
| 192 | + // Stop all reactivity when view is destroyed. |
| 193 | + this._trackerHandles.forEach((tracker) => { |
| 194 | + try { |
| 195 | + tracker.stop() |
| 196 | + } catch (e) { |
| 197 | + console.error(e, tracker) |
| 198 | + } |
| 199 | + }) |
| 200 | + this._trackerHandles = null |
| 201 | + this._meteorActive = false |
| 202 | + }, |
| 203 | + |
| 204 | + $addReactiveSub (key, options) { |
| 205 | + let handle, unwatch |
| 206 | + let subscribe = params => { |
| 207 | + handle = this.$subscribe(key, ...params) |
| 208 | + } |
| 209 | + |
| 210 | + if (typeof options === 'function') { |
| 211 | + if (isServer) { |
| 212 | + subscribe(options.bind(this)()) |
| 213 | + } else { |
| 214 | + unwatch = this.$watch(options, params => { |
| 215 | + subscribe(params) |
| 216 | + }, { |
| 217 | + immediate: true, |
| 218 | + }) |
| 219 | + } |
| 220 | + } else { |
| 221 | + subscribe(options) |
| 222 | + } |
| 223 | + |
| 224 | + return () => { |
| 225 | + if (unwatch) unwatch() |
| 226 | + if (handle) this.$stopHandle(handle) |
| 227 | + }
F438
|
| 228 | + }, |
| 229 | + |
| 230 | + $addMeteorData (key, func) { |
| 231 | + const hasDataField = hasProperty(this.$data, key) |
| 232 | + if (!hasDataField && !hasProperty(this, key) && !hasProperty(this.$props, key)) { |
| 233 | + Object.defineProperty(this, key, { |
| 234 | + get: () => this.$data.$meteor.data[key], |
| 235 | + enumerable: true, |
| 236 | + configurable: true, |
| 237 | + }) |
| 238 | + } |
| 239 | + |
| 240 | + const setData = value => { |
| 241 | + set(hasDataField ? this.$data : this.$data.$meteor.data, key, value) |
| 242 | + } |
| 243 | + |
| 244 | + setData(null) |
| 245 | + |
| 246 | + // Function run |
| 247 | + const run = (params) => { |
| 248 | + let result = func(params) |
| 249 | + if (result && typeof result.fetch === 'function') { |
| 250 | + result = result.fetch() |
| 251 | + } |
| 252 | + if (Vue.config.meteor.freeze) { |
| 253 | + result = Object.freeze(result) |
| 254 | + } |
| 255 | + setData(result) |
| 256 | + } |
| 257 | + |
| 258 | + // Meteor autorun |
| 259 | + let computation |
| 260 | + const unautorun = () => { |
| 261 | + if (computation) this.$stopHandle(computation) |
| 262 | + } |
| 263 | + const autorun = () => { |
| 264 | + unautorun() |
| 265 | + computation = this.$autorun(() => { |
| 266 | + run() |
| 267 | + }) |
| 268 | + } |
| 269 | + |
| 270 | + // Vue autorun |
| 271 | + const unwatch = this.$watch(autorun, noop, { |
| 272 | + immediate: true, |
| 273 | + }) |
| 274 | + |
| 275 | + return () => { |
| 276 | + unwatch() |
| 277 | + unautorun() |
| 278 | + } |
| 279 | + }, |
| 280 | + }, |
| 281 | + }) |
| 282 | + |
| 283 | + if (vueVersion === '2') { |
| 284 | + // Components |
| 285 | + Vue.component('MeteorData', CMeteorData) |
| 286 | + Vue.component('meteor-data', CMeteorData) |
| 287 | + Vue.component('MeteorSub', CMeteorSub) |
| 288 | + Vue.component('meteor-sub', CMeteorSub) |
| 289 | + } |
| 290 | + }, |
| 291 | +} |
| 292 | + |
| 293 | +export const MeteorData = CMeteorData |
| 294 | +export const MeteorSub = CMeteorSub |
0 commit comments