From my understanding, Turbolinks does't not compatible nicely with these MV* frameworks. In the current template with --webpack=vue option, Turbolinks is still enable by default. I think Turbolinks should be turn off if the rails project is init with these MV* options.
@soapsign Turbolinks integrates well, it's just that you have to use Turbolinks DOM events. For ex, normally, we would use DOMContentLoaded, but this would change in case of turbolinks to,
// document.addEventListener('DOMContentLoaded', () => {
document.addEventListener('turbolinks:load', function() {
const app = new Vue({
el: '#vue-app',
template: '<App/>',
components: { App }
})
console.log(app)
})
It's same for React and Angular. Obviously, if you use things like react-router or any client side router having turbolinks wouldn't make sense. You could then manually remove the gem in that case :)
Or you could also pass -J option along-with rails new app_name -J --webpack=vue to not add turbolinks.
OK. Maybe we should change the generated example and comment about the action to be taken if people is going to include any client side router. Btw, great work for the webpack integration for rails. 👍
Yeah that sounds like a good idea @soapsign 👍 Feel free to make a PR if you want :)
@gauravtiwari
document.addEventListener('turbolinks:load', function() {
const app = new Vue({
el: '#vue-app',
template: '<App/>',
components: { App }
})
console.log(app)
})
Won't this code warn on every page if id #vue-app is not present on that page? How do we tackle that?
@ankitrg Just check if node is present on that page and if it does then bootstrap the vue component.
if (document.getElementById('vue-app')) {
document.addEventListener('turbolinks:load', () => {
Vue({
el: '#vue-app',
template: '<App/>',
components: { App },
});
});
}
You could also dynamically check certain id or class name and bootstrap based on that (to avoid duplication).
@gauravtiwari I was planning to do this.
Can you please tell me more about what you meant by:
You could also dynamically check certain id or class name and bootstrap based on that (to avoid duplication).
Cool 👍 Sure @ankitrg It would require a bit more setup though. It's sort of based on the technique react-rails and few other similar gems uses. Loosely you can do something like this,
<template>
<div id="app">
<p>{{ message }}</p>
</div>
</template>
<script>
// Vue component
const HelloVue = {
data: function () {
return {
message: "Hello Vue!"
}
}
}
window.HelloVue = HelloVue;
module.exports = HelloVue;
</script>
<style scoped>
p {
font-size: 2em;
text-align: center;
}
</style>
<div id="vue-component" data-vue-component="HelloVue"></div>
const vueNode = document.getElementById('vue-component');
if (vueNode) {
const componentName = vueNode.getAttribute('data-vue-component');
// Assigned the class object to window earlier
const componentConstructor = window[componentName];
document.addEventListener('turbolinks:load', () => {
Vue({
el: '#vue-app',
template: `<${componentName} />`,
components: { componentConstructor },
});
});
}
But, not sure this would work as it is. Probably, you might bump into some issues :)
@ankitrg This works
import Vue from 'vue'
import App from './app.vue'
window.HelloVue = App;
/*
// May be make a register.js file that does this and include it to main packs file
Register components globally
window.ComponentX = X;
window.ComponentY = Y;
*/
document.addEventListener('DOMContentLoaded', () => {
const vueNode = document.getElementById('vue-component');
if (vueNode) {
const componentName = vueNode.getAttribute('data-vue-component');
// Assigned the class object to window earlier
const VueComponent = window[componentName];
document.addEventListener('turbolinks:load', () => {
const app = new Vue({
el: vueNode,
template: `<VueComponent/>`,
components: { VueComponent },
});
console.log(app);
});
}
});
@gauravtiwari This looks awesome. I still have two questions if you don't mind answering:
DOMContentLoaded and turbolinks:load wrapper?@gauravtiwari Can we also achieve this using Vue.Component, like:
Vue.component('my-component', {
// options
})
register a global component, you can use Vue.component(tagName, options)
This is what the doc says.
Then I can just use <my-component> in my view, no need to refer it by id? Please tell me if I am missing something, I am also new to Vue.
You don't. If you use turbolinks:load you don't need DOMContentLoaded and vice-versa.
That's right. But you can customise that - getElementById will return array if there are multiple nodes with same id.
const nodes = document.getElementById('vue-component');
for (let i = 0; i < nodes.length; ++i) {
var vueNode = nodes[i];
var componentName = vueNode.getAttribute('data-vue-component');
// Assigned the class object to window earlier
const VueComponent = window[componentName];
document.addEventListener('turbolinks:load', () => {
const app = new Vue({
el: vueNode,
template: `<VueComponent/>`,
components: { VueComponent },
});
console.log(app);
});
}
@ankitrg It seems you can do that. Feel free to try it out and see how it goes :) Have fun!
I'm having some trouble getting this to work -- using the example code, it loads properly on the first page load, but if I navigate away and come back via turbolinks, the vue component no longer renders (in fact, the component stays loaded the entire time). In React I can unload and load components on the fly after turbolinks:load -- can/should I do the same here?
@andersodt Yes, that's right. I think you have mount/unmount like react - https://vuejs.org/v2/api/#vm-destroy and https://vuejs.org/v2/guide/instance.html#Lifecycle-Diagram
@gauravtiwari Thanks, your solution pointed me in the right direction. My only issue now is that I seem to have a race condition where this snippet is trying to create the view root app before my component webpack has loaded from the server. Any ideas?
I think I have found a simpler and more hassle-free solution, i.e. page specific javascript. Just use <%= javascript_pack_tag 'forms_new' %> at the bottom of the page on which you have the particular element and thus you will not need to do any other thing.
Is there a problem with this solution?
I think the issue is that, if you were to navigate away from the page with Turbolinks, the Vue components would still exist in memory. You'd still have manually unmount them.
Is that supposed to be a problem? Please pardon me if I am naive! Here are the lines from the turbolinks doc:
When you navigate to a new page, Turbolinks looks for any
I'm not sure about the memory leak, waiting to hear from someone who has experience with this. I know about the react-rails gem and how they do this but can you tell me what is webpacker-react? I don't think it's another gem, it should be the react configured via webpacker. In that case, if we find out how they are doing it we can also follow the same strategy.
Edit
@andersodt I also feel that if the component was still in the memory it will throw a warning about not being able to find the element with the given id! What do you think?
https://github.com/renchap/webpacker-react
This adds a couple things, like the aforementioned component loading/unloading, to make it easier to use webpacker and react.
You would need to destroy the component before mounting it again. There are lifecycle hooks for Vue.js, similar to react - https://vuejs.org/v2/api/#vm-destroy. Obviously, you would need to do this all yourself, apparently there is no gem like webpacker-vue yet :)
You can re-purpose this UJS - https://github.com/renchap/webpacker-react/blob/master/javascript/webpacker_react-npm-module/src/ujs.js to mount/unmount, but for Vue.js using vue hooks.
document.addEventListener('turbolinks:load', () => {
const vueApp = new Vue({
el: vueNode,
template: `<VueComponent/>`,
components: { VueComponent },
});
// Register globally
window.vueApp = vueApp;
});
// Like so (could be made better)
document.addEventListener('turbolinks:before-render', () => {
window.vueApp.$destroy()
})
I think doing something like this will be more clean:
const vueApp = new Vue({
components: { VueComponent },
});
document.addEventListener('turbolinks:load', () => (vueApp) => {
vueApp.$mount( '#node-id')
})
document.addEventListener('turbolinks:before-render', (vueApp) => {
vueApp.$destroy()
})
I think there's no problem with Vue and turbolinks. In fact, it does not need an eventListener for both turbolinks:load and DOMContentLoaded
https://github.com/rails/webpacker/pull/240 can someone verify this?
@ytbryan If this is the case I am most happy! I have used it in such a way and it works fine. The only doubts I have is about memory leaks! If someone with more knowledge could speak on this.
@elsurudo I am also facing the same condition. My vue component works sometime and other times it doesn't I am doing page specific load for js. Did you find anything?
Only for the record. Assuming you have a div with id="app".
./bin/yarn add vue-turbolinks See
import VueAdapter from 'vue-turbolinks'
document.addEventListener('turbolinks:load', () => {
const app = new Vue({
el: '#app',
template: '<App/>',
components: { App },
mixins: [VueAdapter],
beforeMount() {
console.log(`beforeMount is called.`)
},
mounted() {
console.log("mounted is called")
},
destroyed() {
console.log("destroyed is called.")
}
})
console.log(app)
})
This will unmount the vue-component proper. And again, from my quick manual tests, i don't think dom event listener is required (might be wrong again :| ).
As for memory issue, it's not turbolinks fault. it's a work in progress. See https://github.com/vuejs/vue/issues/5380
cc #240
Hi,
I was wondering if something had changed and Turbolinks needed to be disabled because I am getting this warning while using with React & Turbolinks:
Warning: unmountComponentAtNode(): The node you're attempting to unmount was rendered by another copy of React.
I have checked all over Google the web and can't seem to make it work. The solution that seems to work for most people doesn't work for me:
function mountComponent(){
ReactDOM.render(
<Hello name="React" />,
document.getElementById('app'),
)
}
function unmountComponent(){
ReactDOM.unmountComponentAtNode(document.getElementById('app'))
}
document.addEventListener('turbolinks:load', mountComponent, {once: true})
document.addEventListener('turbolinks:render', mountComponent)
document.addEventListener('turbolinks:before-render', unmountComponent)
I also tried this one:
document.addEventListener('turbolinks:load', mountComponent)
document.addEventListener('turbolinks:before-cache', unmountComponent)
No luck so far. Any suggestions?
Do you think it's better to create a new issue? @iamraffles
Well... I was going to!
But I found this one and wanted to check if this still held true (not having to disable Turbolinks) instead opening one and someone saying "You can't use this with Turbolinks enabled, you crazy person!".
But you're right! I'll open a new issue, it makes more sense to go that route.
My bad!
Thanks for the new issue.
IMO turbolinks is great when one understands how it works and why.
Let's iron out the gotchas. You should absolutely be able to use Turbolinks with any of these other frameworks.
On May 7, 2017, at 01:18, Bryan Lim notifications@github.com wrote:
Thanks for the new issue.
IMO turbolinks is great when one understands how it works and why.
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub, or mute the thread.
Can you please give me an example on how to use it with angular?
@syedfazilbasheer-quester You will need do something like this:
import { bootstrap } from '@angular/platform-browser-dynamic';
// Standard angular component
import HelloAngularComponent from '../hello-angular';
document.addEventListener('turbolinks:load', () => {
bootstrap(HelloAngularComponent);
});
Do you think https://github.com/jeffreyguenther/vue-turbolinks "solves" the problem by any means?
You can watch the video that explains it as well.
So I added bootstrap's css and javascript in app/javascript/application.js:
import 'bootstrap/dist/css/bootstrap.css';
import 'bootstrap/dist/css/bootstrap-theme.css';
import 'bootstrap/dist/js/bootstrap.min';
And according to some solutions I've added jquery.turbolinks to app/assets/javascripts/application.js too:
//= require jquery
//= require jquery.turbolinks
...
// = require turbolinks
//= require_tree .
However, the bootstrap drop down menu works intermittently when navigating between pages.
Does anyone has a work around for this cause bootstrap is only problem I have since I use turbolinks:load events for all my custom bindings.
Example:
$(document).on('turbolinks:load', function() {
$('table[role="datatable"]').each(function(){
$(this).DataTable({});
});
});
@resting I have problem when importing bootstrap in app/javascripts/application.js, some features not working including dropdown menu. When I remove import from there and put require on app/assets/javascripts/application.js it can without problem for all bootstrap features.
@maengkom Good to hear that. Can you share how you require bootstrap from app/assets/javascripts/application.js? Does that mean you're not installing bootstrap through yarn but using bootstrap-sass perhaps?
Here are package for webpacker, that integrate turbolinks with vue.js
I want to use Vue only on specific pages. This is what works for me.
I know this is closed. Just chipping this in in case there are others who don't want another dependency (vue-turbolinks).
Add div id="products"></div> and the pack_tags to views/products/index.html.
import Vue from 'vue'
import App from '../app.vue'
const vueApp = new Vue({
render: h => h(App)
})
function mountVueApp () {
vueApp.$mount('#products')
}
function destroyVueApp () {
vueApp.$destroy()
document.removeEventListener('turbolinks:load', mountVueApp)
document.removeEventListener('turbolinks:before-render', destroyVueApp)
}
document.addEventListener('turbolinks:load', mountVueApp)
document.addEventListener('turbolinks:before-render', destroyVueApp)
I have tried the above steps for my angular app. Please, anyone, help me I am getting zone error. How can I use turbolinks? I have following directory structure.
app/javascript/hello_angular and app/javascript/app_books. In app/javascript/hello_angular I have app directory (app.component.ts, html, app.module.ts) and (index.ts, polyfills.ts). In app.component.html I have <a href="/books">link to books</a> it renders my app/views/books/index.html which has <div><app_books></app_books></div><%= javascript_pack_tag 'app_books' %> But unfortunately I am getting Uncaught Error: Zone already loaded. Can anyone help me out ?
I don't think turbolinks would work well with SinglePageApps that have their own methods for routing.
For your case, I would take a look at https://angular.io/api/router. For the above steps, React/Vue can mount/unmount with little friction from turbolinks. Angular has a bunch of helpers that need to be properly torn down, like ngZone.
Apart from that, using a SPA (turbolinks) with other SPAs (React/Vue/Angular) is not optimal since you need to tear down the nested SPA on every single render.
Most helpful comment
@soapsign Turbolinks integrates well, it's just that you have to use Turbolinks DOM events. For ex, normally, we would use
DOMContentLoaded, but this would change in case of turbolinks to,It's same for React and Angular. Obviously, if you use things like
react-routeror any client side router having turbolinks wouldn't make sense. You could then manually remove the gem in that case :)