├── .gitignore ├── .versions ├── README.md ├── blaze-template.js ├── install.js ├── main.js ├── package.js ├── router-link.html ├── router-link.js ├── vue-component.html └── vue-component.js /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meteor-vue/blaze-integration/00e5559b7099095d98f211482da62247afb4ed21/.gitignore -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | babel-compiler@7.7.0 2 | babel-runtime@1.5.0 3 | base64@1.0.12 4 | blaze@2.5.0 5 | blaze-tools@1.1.0 6 | caching-compiler@1.2.2 7 | caching-html-compiler@1.2.0 8 | check@1.3.1 9 | coffeescript@2.4.1 10 | coffeescript-compiler@2.4.1 11 | diff-sequence@1.1.1 12 | dynamic-import@0.7.2 13 | ecmascript@0.16.0 14 | ecmascript-runtime@0.8.0 15 | ecmascript-runtime-client@0.12.1 16 | ecmascript-runtime-server@0.11.0 17 | ejson@1.1.1 18 | fetch@0.1.1 19 | html-tools@1.1.0 20 | htmljs@1.1.0 21 | inter-process-messaging@0.1.1 22 | meteor@1.10.0 23 | modern-browsers@0.1.7 24 | modules@0.17.0 25 | modules-runtime@0.12.0 26 | mongo-id@1.0.8 27 | observe-sequence@1.0.16 28 | ordered-dict@1.1.0 29 | peerlibrary:computed-field@0.10.0 30 | peerlibrary:data-lookup@0.3.0 31 | promise@0.12.0 32 | random@1.2.0 33 | react-fast-refresh@0.2.0 34 | reactive-var@1.0.11 35 | spacebars@1.2.0 36 | spacebars-compiler@1.2.0 37 | templating@1.4.1 38 | templating-compiler@1.4.1 39 | templating-runtime@1.5.0 40 | templating-tools@1.2.0 41 | tracker@1.2.0 42 | underscore@1.0.10 43 | vuejs:blaze-integration@0.3.2 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Vue and Blaze integration 2 | ========================= 3 | 4 | This Meteor package provides Vue components and Blaze templates for easier integration of [Vue](https://vuejs.org/) 5 | and [Blaze](http://blazejs.org) in the same application, with full reactivity across boundaries. 6 | 7 | Adding this package to your Meteor application provides: 8 | * `blaze-template` Vue component, to render Blaze templates from Vue 9 | * `VueComponent` Blaze template, to render Vue components from Blaze 10 | * `RouterLink` Blaze template, to render links for [Vue Router](https://router.vuejs.org/en/) in Blaze templates 11 | 12 | **You have to use [Tracker-enabled fork of Vue](https://github.com/meteor-vue/vue) and 13 | [fork of Tracker](https://github.com/meteor-vue/tracker).** 14 | See [these instructions for more information](https://github.com/meteor-vue/guide). 15 | 16 | Installation 17 | ------------ 18 | 19 | ``` 20 | meteor add vuejs:blaze-integration 21 | ``` 22 | 23 | blaze-template 24 | -------------- 25 | 26 | From Vue, you can render Blaze templates with `` Vue component: 27 | 28 | ```vue 29 | 30 | ``` 31 | 32 | For example, Blaze template could be: 33 | 34 | ```handlebars 35 | 38 | ``` 39 | 40 | This example also binds the data context to the template. So you can pass data from Vue to Blaze template. 41 | 42 | You can bind the property to make template dynamic, bound to `templateData` reactive Vue property: 43 | 44 | ```vue 45 | 46 | ``` 47 | 48 | You can use a different tag for the wrapper tag (default is `div`): 49 | 50 | ```vue 51 | 52 | ``` 53 | 54 | Any extra arguments are passed on to wrapper tag: 55 | 56 | ```vue 57 | 58 | ``` 59 | 60 | If you put any content between start and end tag, it is passed to the Blaze template as `Template.contentBlock`: 61 | 62 | ```vue 63 | 64 |

This is slot content from Vue: {{slotValue}}

65 |
66 | ``` 67 | 68 | Example Blaze template: 69 | 70 | ```handlebars 71 | 75 | ``` 76 | 77 | It is reactive as well. `slotValue` comes from the Vue component, but it is rendered inside Blaze 78 | block helper template. 79 | 80 | VueComponent 81 | ------------ 82 | 83 | From Blaze, you can render Vue components with `VueComponent` Blaze template: 84 | 85 | ```handlebars 86 | {{> VueComponent component="my-vue-component" props=props}} 87 | ``` 88 | 89 | This example also binds props to the component. So you can pass data from Blaze to Vue component. 90 | Reactively, by having `props` reactively change. 91 | 92 | If you have to provide any extra arguments to the component's constructor, you can do that 93 | using `args`. For example, `args` value could be `{store}`, to pass 94 | [Vuex store](https://vuex.vuejs.org/en/) to the component. 95 | 96 | RouterLink 97 | ---------- 98 | 99 | If you use Vue Router in your application, you might want to change pages from Blaze. 100 | Simply using `` will not work well because it will trigger the whole application 101 | to reload on location change. Instead, you can use `RouterLink` Blaze template: 102 | 103 | ```handlebars 104 | {{#RouterLink to="/"}}Go to Home{{/RouterLink}} 105 | ``` 106 | 107 | This will render a link which will on click trigger page change through router. 108 | 109 | The template aims to be equivalent to [`` Vue component](https://router.vuejs.org/en/api/router-link.html). 110 | For example, you can pass as `to` an object which will be resolved to the target location. 111 | 112 | It also supports adding attributes, you can add any attribue that is not known as an option for [`` Vue component](https://router.vuejs.org/en/api/router-link.html). 113 | 114 | ```handlebars 115 | {{#RouterLink to="/" class="my-class" data-my-info="{info: true}"}} 116 | ``` 117 | -------------------------------------------------------------------------------- /blaze-template.js: -------------------------------------------------------------------------------- 1 | import {_} from 'meteor/underscore'; 2 | import {Blaze} from 'meteor/blaze'; 3 | import {EJSON} from 'meteor/ejson'; 4 | 5 | const templateTypes = [String, Object]; 6 | 7 | function normalizeVnode(vnode) { 8 | vnode = _.pick(vnode, 'children', 'tag', 'key', 'isComment', 'isStatic', 'text', 'raw', 'ns', 'data', 'componentOptions'); 9 | vnode.children = _.map(vnode.children, normalizeVnode); 10 | vnode.data = _.omit(vnode.data, 'hook', 'on', 'pendingInsert'); 11 | return vnode; 12 | } 13 | 14 | function normalizeVnodes(vnodes) { 15 | return _.map(vnodes, normalizeVnode); 16 | } 17 | 18 | function vnodesEquals(vnodes1, vnodes2) { 19 | return EJSON.equals(normalizeVnodes(vnodes1), normalizeVnodes(vnodes2)); 20 | } 21 | 22 | export default { 23 | name: 'blaze-template', 24 | 25 | props: { 26 | template: { 27 | type: templateTypes, 28 | required: true, 29 | }, 30 | tag: { 31 | type: String, 32 | default: 'div' 33 | }, 34 | data: Object, 35 | }, 36 | 37 | data() { 38 | return { 39 | renderedSlot: null, 40 | }; 41 | }, 42 | 43 | methods: { 44 | getTemplate() { 45 | let template = this.template; 46 | if (_.isString(template)) { 47 | template = Blaze._getTemplate(template, null); 48 | } 49 | 50 | if (!template) { 51 | throw new Error(`Blaze template '${this.template}' not found.`); 52 | } 53 | 54 | return template; 55 | }, 56 | 57 | updateSlot() { 58 | if (this.$slots.default) { 59 | // To prevent observer to be setup on vnodes. 60 | this.$slots.default._isVue = true; 61 | } 62 | this.renderedSlot = this.$slots.default; 63 | }, 64 | 65 | renderTemplate() { 66 | if (this.blazeView) { 67 | Blaze.remove(this.blazeView); 68 | this.blazeView = null; 69 | } 70 | 71 | // To make it available before we start rendering the Blaze template. 72 | this.updateSlot(); 73 | 74 | const vm = this; 75 | this.blazeView = Blaze.renderWithData(this.getTemplate().constructView(function () { 76 | // This function defines how "Template.contentBlock" gets rendered inside block helpers. 77 | // It does not provide any Blaze content (returns null), but after Blaze renders it, 78 | // it uses Vue vdom patching to render slot content. 79 | 80 | const view = this; 81 | 82 | if (!view.isRendered) { 83 | view.onViewReady(function () { 84 | view.autorun(function (computation) { 85 | const newVnodes = vm.renderedSlot; 86 | const prevVnodes = view._vnodes; 87 | view._vnodes = newVnodes; 88 | 89 | // To prevent unnecessary reruns of the autorun if patch registers any dependency. 90 | // The only dependency we care about is on "renderedSlot" which has already been established. 91 | Tracker.nonreactive(function () { 92 | if (!prevVnodes) { 93 | _.each(newVnodes, function (vnode, i, list) { 94 | // We prepend rendered vnodes before "lastNode" in order. 95 | // So rendered content is in fact between "firstNode" and "lastNode". 96 | const el = vm.__patch__(null, vnode, false, false); 97 | view._domrange.parentElement.insertBefore(el, view.lastNode()); 98 | }); 99 | } 100 | else { 101 | _.each(_.zip(prevVnodes, newVnodes), function (vnodes, i, list) { 102 | vm.__patch__(vnodes[0], vnodes[1]); 103 | }); 104 | } 105 | }) 106 | }); 107 | }); 108 | view.onViewDestroyed(function () { 109 | if (vm._vnodes) { 110 | _.each(vm._vnodes, function (vnode, i, list) { 111 | vm.__patch__(vnode, null); 112 | }); 113 | vm._vnodes = null; 114 | } 115 | }); 116 | } 117 | 118 | return null; 119 | }), () => this.data, this.$el, null, this); 120 | this.renderedToElement = this.$el; 121 | }, 122 | }, 123 | 124 | watch: { 125 | // Template has changed. 126 | template(newValue, oldValue) { 127 | this.renderTemplate(); 128 | }, 129 | }, 130 | 131 | created() { 132 | // So that we can use this Vue component instance as a parent view to the Blaze template. 133 | this._scopeBindings = {}; 134 | 135 | this.renderedToElement = null; 136 | }, 137 | 138 | render(createElement) { 139 | return createElement(this.tag); 140 | }, 141 | 142 | updated() { 143 | // We rerender the template when primary element changes (when tag changes). 144 | if (this.renderedToElement !== this.$el) { 145 | this.renderTemplate(); 146 | } 147 | // Slot changed. 148 | else if (!vnodesEquals(this.renderedSlot, this.$slots.default)) { 149 | this.updateSlot(); 150 | } 151 | }, 152 | 153 | // Initial rendering. 154 | mounted() { 155 | this.renderTemplate(); 156 | }, 157 | 158 | destroyed() { 159 | if (this.blazeView) { 160 | Blaze.remove(this.blazeView); 161 | this.blazeView = null; 162 | } 163 | }, 164 | } 165 | -------------------------------------------------------------------------------- /install.js: -------------------------------------------------------------------------------- 1 | import BlazeTemplate from './blaze-template'; 2 | 3 | export function install(Vue, options) { 4 | Vue.component('blaze-template', BlazeTemplate); 5 | } 6 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import VueBlazeTemplate from './install'; 4 | 5 | Vue.use(VueBlazeTemplate); 6 | 7 | if (Package['akryum:vue-router'] || Package['akryum:vue-router2']) { 8 | import './router-link'; 9 | } 10 | 11 | import './vue-component'; -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'vuejs:blaze-integration', 3 | version: '0.3.2', 4 | summary: "Vue integration with Meteor's Blaze rendering engine." 5 | }); 6 | 7 | Package.onUse(function(api) { 8 | api.versionsFrom('1.8.1'); 9 | 10 | api.use([ 11 | 'ecmascript', 12 | 'blaze@2.5.0', 13 | 'templating@1.4.1', 14 | 'ejson', 15 | 'underscore', 16 | 'peerlibrary:data-lookup@0.3.0' 17 | ]); 18 | 19 | api.use([ 20 | 'akryum:vue-router@0.2.2', 21 | 'akryum:vue-router2@0.2.3' 22 | ], {weak: true}); 23 | 24 | api.mainModule('main.js', 'client'); 25 | }); 26 | -------------------------------------------------------------------------------- /router-link.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /router-link.js: -------------------------------------------------------------------------------- 1 | import './router-link.html'; 2 | 3 | // Top view is a Vue component instance. 4 | function topView(view) { 5 | while (view.originalParentView || view.parentView) { 6 | view = view.originalParentView || view.parentView; 7 | } 8 | return view; 9 | } 10 | 11 | const routerLinkOptions = [ 12 | 'to', 'replace', 'append', 'tag', 'active-class', 'exact', 'event', 'exact-active-class', 13 | ]; 14 | 15 | Template.RouterLink.helpers({ 16 | href() { 17 | const vm = topView(Template.instance().view); 18 | const current = vm.$route; 19 | const args = Template.currentData(); 20 | const {href} = vm.$router.resolve(args.to, current, args.append); 21 | return href; 22 | }, 23 | attributes() { 24 | const args = Template.currentData(); 25 | return Object.keys(args) 26 | .filter(argName => routerLinkOptions.indexOf(argName) === -1) 27 | .reduce((attrs, key) => { 28 | return Object.assign(attrs, { [key]: args[key] }); 29 | }, {}); 30 | }, 31 | }); 32 | 33 | // Taken from vue-router's router-link component's code. 34 | function guardEvent(event) { 35 | // Don't redirect with control keys. 36 | if (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey) return; 37 | // Don't redirect when preventDefault called. 38 | if (event.defaultPrevented) return; 39 | // Don't redirect on right click. 40 | if (event.button !== undefined && event.button !== 0) return; 41 | // Don't redirect if `target="_blank"`. 42 | if (event.currentTarget && event.currentTarget.getAttribute) { 43 | const target = event.currentTarget.getAttribute('target'); 44 | if (/\b_blank\b/i.test(target)) return; 45 | } 46 | // This may be a Weex event which doesn't have this method. 47 | if (event.preventDefault) { 48 | event.preventDefault(); 49 | } 50 | return true; 51 | } 52 | 53 | Template.RouterLink.events({ 54 | 'click a'(event) { 55 | // Some other anchor's click? 56 | if (Template.instance().firstNode !== event.currentTarget) return; 57 | 58 | if (!guardEvent(event)) return; 59 | 60 | const vm = topView(Template.instance().view); 61 | const current = vm.$route; 62 | const router = vm.$router; 63 | const args = Template.currentData(); 64 | 65 | const {location} = vm.$router.resolve(args.to, current, args.append); 66 | 67 | if (args.replace) { 68 | router.replace(location); 69 | } 70 | else { 71 | router.push(location); 72 | } 73 | } 74 | }); 75 | -------------------------------------------------------------------------------- /vue-component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vue-component.js: -------------------------------------------------------------------------------- 1 | import {_} from 'meteor/underscore'; 2 | import {EJSON} from 'meteor/ejson'; 3 | 4 | import {DataLookup} from 'meteor/peerlibrary:data-lookup'; 5 | 6 | import Vue from 'vue'; 7 | 8 | import './vue-component.html'; 9 | 10 | function removeVm(vm) { 11 | vm.$destroy(); 12 | if (vm.$el) { 13 | vm.$el.remove(); 14 | } 15 | } 16 | 17 | Template.VueComponent.onRendered(function () { 18 | this.autorun((computation) => { 19 | if (this.vm) { 20 | removeVm(this.vm); 21 | this.vm = null; 22 | } 23 | 24 | // Using DataLookup to depend only on "component" value from the data context. 25 | // We do not want to recreate the whole component unnecessarily. 26 | let component = DataLookup.get(() => Template.currentData(this.view), 'component', (a, b) => a === b) || null; 27 | if (_.isString(component)) { 28 | const componentName = component; 29 | component = Vue.component(component); 30 | if (!component) { 31 | throw new Error(`Component '${componentName}' not found.`); 32 | } 33 | } 34 | else if (component) { 35 | component = Vue.extend(component); 36 | } else { 37 | throw new Error("Component not provided."); 38 | } 39 | 40 | // Extra arguments to be passed to the component's constructor. 41 | // Any change in them recreate the whole component. 42 | let args = DataLookup.get(() => Template.currentData(this.view), 'args', EJSON.equals) || {}; 43 | 44 | // It can be any element, because it gets replaced by Vue. 45 | const el = document.createElement('div'); 46 | this.view._domrange.parentElement.insertBefore(el, this.view.lastNode()); 47 | 48 | // Initial set of non-reactive props. 49 | let propsData = Tracker.nonreactive(() => DataLookup.lookup(Template.currentData(this.view), 'props')); 50 | 51 | // To prevent unnecessary reruns of the autorun if constructor registers any dependency. 52 | // The only dependency we care about is on "component" which has already been established. 53 | this.vm = Tracker.nonreactive(() => { 54 | return new component(_.extend({ 55 | el, 56 | propsData, 57 | }, args)); 58 | }); 59 | 60 | // And now observe props and update them if they change. 61 | this.autorun((computation) => { 62 | const props = DataLookup.get(() => Template.currentData(this.view), 'props', EJSON.equals) || {}; 63 | _.each(_.keys(this.vm._props || {}), (key, i) => { 64 | const isDefined = !_.isUndefined(props[key]); 65 | const isPreviouslyDefined = propsData && !_.isUndefined(propsData[key]); 66 | if (isDefined || (!isDefined && isPreviouslyDefined)) { 67 | this.vm._props[key] = props[key]; 68 | } 69 | }); 70 | propsData = props; 71 | }); 72 | }); 73 | }); 74 | 75 | Template.VueComponent.onDestroyed(function () { 76 | if (this.vm) { 77 | removeVm(this.vm); 78 | this.vm = null; 79 | } 80 | }); --------------------------------------------------------------------------------