I've got this big legacy web app based on Codeigniter and jQuery.
The plan is to phase out jQuery and start using Vuejs instead. We are replacing certain parts of the website step by step.
I have just installed Nuxtjs and got the file structure the way i like it inside the legacy project.
Now to the question. How can i access a Vue component from one of my legacy javascript files?
App.vue
<template>
<div id="app">
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App" />
</div>
</template>
<script lang="ts">
import Vue from "vue";
import HelloWorld from "./components/HelloWorld.vue";
export default Vue.extend({
name: "App",
components: {
HelloWorld
}
});
</script>
main.ts
import Vue from "vue";
import App from "./App.vue";
import store from "./store";
Vue.config.productionTip = false;
new Vue({
store,
render: h => h(App)
}).$mount("#app");
I want to display my App.vue inside an ordinary php/html view.
I am doing something similar right now, the project was originally written using Thymeleaf and jQuery and now we are switching to Vue.
You can communicate between vue components and non-vue components in a few ways, none of them are "pretty".
Communication
Classic JavaScript events
Pretty straightforward
// legacy code
document.dispatchEvent(new CustomEvent('legacy-vue.something-happened', { detail: payload }))
// vue component
created () {
document.addEventListener('legacy-vue.something-happened', this.onSomethingHappened)
},
destroyed () { // don't forget to remove the listener!
document.removeEventListener('legacy-vue.something-happened', this.onSomethingHappened)
}
Exposing EventHub
Similar to the previous one, but you are using vue events instead. This is what i would recommend, because it's the Vue way of handling events and your goal is to vuetify your app.
// initialization
const hub = new Vue()
Vue.prototype.$eventHub = hub
window.$vueEventHub = hub
// legacy code
$vueEventHub.$emit('something-happened', payload)
// vue component
created () {
this.$eventHub.$on('something-happened', this.onSomethingHappened)
},
destroyed () {
this.$eventHub.$off('something-happened', this.onSomethingHappened)
}
Exposing whole components
The most flexible way, but it's hard to see what's going on where. Event based approaches the lesser evil in my opinion (it's easy to track events).
// vue component
created () {
window.vueTableComponent = this
}
// legacy component
vueTableComponent.fetchNextPage()
vueTableComponent.registerOnPageFetchedCallback(callback);
Summary
No matter which approach you pick, i recommend doing something like this:
Let's say that you have TableComponent. TableComponent has few props like apiUrl, emits row-clicked event, etc. It would be best to design the component without thinking about your legacy code at all, and then create it's legacy wrapper because at one point it will be used both with vue-only screens and with mixed-screens (with both legacy components and vue components). An example:
LegacyTableComponentWrapper.vue
<template>
<table-component
:api-path="apiPath"
#row-clicked="onRowClicked"
ref="table-component"
/>
</template>
export default {
data: () => ({
apiPath: null
}),
methods: {
onRowClicked (row) {
this.$eventHub.$emit('table-row-clicked', row) // notify legacy code
},
onApiPathDefined (payload) {
this.apiPath = payload
}
},
mounted () {
// legacy code might require the TableComponent to act differently
// and if you don't want the TableComponent to know whenever it's legacy or not
// you can always override one or more of it's methods.
this.$refs['table-component'] = this.overriddenImplementationOfXYZ
},
created () {
this.$eventHub.$on('define-api-path', this.onApiPathDefined)
},
destroyed () {
this.$eventHub.$off('define-api-path', this.onApiPathDefined)
}
}
It's sure more work at first, but will save you a headache later, when you will be working on your first view which is fully in vue and all that legacy communication stuff is in the way.
Related
I have a rather specific question.
I'm using vue in my rails application through rails webpacker, to use vue components, I have to put a javascript pack tag in my layout and that references a javascript file that in turn renders the vue component, you can imagine that in total this approach has led me to make a lot of workarounds, but the one thing I still have left is a vue custom directive click-outside that I have had to add to each of my vue component generators, for example, here in filter-products.js
import Vue from "vue";
import filterProducts from "../../views/filter-products";
var element = document.getElementById("filter-products");
const props = JSON.parse(element.getAttribute("props"));
Vue.directive('click-outside', {
bind: function(el, binding, vNode) {
//bind logic
},
unbind: function(el, binding) {
//unbind logic
}
});
if (element != null) {
new Vue({
render: (h) => h(filterProducts, { props }),
}).$mount(element);
}
the custom directive code is actually big, so what I have in mind but am not sure how to do is one of two things:
Have the bulk for that custom directive in an ES6 Module and import that here and just use it directly.
Create a prototype for Vue that includes this custom directive and import it instead of importing vue from "vue".
Is either of the approaches better? and how would I achieve them? thanks!
Create a folder named directives and for each directive create a file to make your code more organized and maintenable especially in team :
import Vue from 'vue';
const directiveName = {
inserted: function(el, binding) {},
update: function(el, binding) {},
};
export default directiveName;
Vue.directive('directiveName', directiveName);//optional
then import it in any component like :
import directiveName from 'path-to-directives-folder/directives/directiveName'
then use it as follows :
data(){
...
},
directives:{directiveName}
I currently have a strange Vue setup due to our websites all using an old system.
What we have had to do is create an instance of Vue for each component (usually not many). What I want to do for all components is to pass their name and reference to the element into an array, just for reference when debugging issues on live issues.
app.js
import Vue from "vue";
import Axios from 'axios';
import inViewportDirective from 'vue-in-viewport-directive';
window.components = [];
Vue.component( 'video-frame', () => import('./components/VideoFrame.vue' /* webpackChunkName: "video-frame" */) );
Vue.prototype.$event = new Vue();
Vue.prototype.$http = Axios;
Array.prototype.forEach.call(document.querySelectorAll(".app"), (el, index) => new Vue({el}));
Now i'm adding the following code to each component, is there not a way I can do this once within my app.js and have all the components automatically do the following:
mounted() {
window.components.push({
tag: this.$vnode.tag,
elm: this.$vnode.elm
});
},
You can use a global mixin like this:
Vue.mixin({
mounted: function() {
window.components.push({
tag: this.$vnode.tag,
elm: this.$vnode.elm
});
}
});
That will ensure that code will run on the mounted hook on every single one of your Vue instances.
Reference: https://v2.vuejs.org/v2/guide/mixins.html
At this project I'm working on there is a legacy server-rendered web page and some components had problems I've been assigned to fix, and I convinced the team to rewrite those parts in Vue to kickstart our migration.
I wrote the whole mini-app using the Webpack template provided by Vue CLI and it works like a charm... in that specific environment.
If I npm run build the built index.html also works fine in a static server.
However, I can't seem to include the app in an existing page composed of many other elements. Shouldn't it be as simple as adding the <div id='myApp'></div> element to the HTML and loading the generated JS files?
If it helps, the legacy app is a Rails app using .erb templates and the JS files are being loaded through the main pipeline in application.js.
Does anyone know why nothing happens when I try this?
Edit: more information - this is how main.js looks before build:
/* eslint-disable */
import Vue from 'vue'
// UI components
import VueSelect from 'vue-select'
import DynamicForm from './components/DynamicForm/'
Vue.component('vue-select', VueSelect)
Vue.config.productionTip = false
const DynamicForms = new Vue({
el: '.dynamic-form',
render: h => h(DynamicForm)
})
Edit: I managed to get Vue to work by integrating Webpack to Rails with Webpacker. However I still have some problems regarding context:
This is my main.js in one of the Vue components. It was working fine until I tried the PropData stunt so I could reuse the component with different data in a few places.
/* eslint-disable */
import Vue from 'vue'
// UI components
import VueSelect from 'vue-select'
// import 'nouislider'
import DynamicForm from './components/DynamicForm/'
import fields from './fields'
import fieldRules from './field-rules'
Vue.component('vue-select', VueSelect)
Vue.config.productionTip = false
document.addEventListener('DOMContentLoaded', () => {
const el = document.createElement('div')
document.querySelector('.dynamic-form').appendChild(el)
const vm = new DynamicForm({
propsData: {
fields,
fieldRules
},
el,
render: h => h(DynamicForm)
})
})
This is DynamicForm/index.vue
<template>
<div id='app'>
<ParamList :fields='paramFields' :fieldRules='paramRules'></ParamList>
<Output></Output>
</div>
</template>
<script>
import Vue from 'vue'
import ParamList from './ParamList'
import Output from './Output'
export default Vue.extend({
props: [ 'fields', 'fieldRules' ],
name: 'DynamicForm',
components: {
ParamList,
Output
},
data () {
return {
paramFields: this.fields,
paramRules: this.fieldRules
}
}
})
</script>
<style>
</style>
The field and fieldData props are merely JSON/JSONP with some data I'm going to use inside those components. The idea is that I could write another main.js changing just the field and fieldData when initing the Vue instance.
What am I doing wrong?
I've managed to fix everything in a three-step change to my components.
Integrate Webpack into Rails using Webpacker. There's even a Vue template!
Change the root component (the one mounted at a real DOM element) to a Vue subclass using Vue.extend (so the module line # the .vue file read export default Vue.extend({ instead of simply export default {
Remove the render function from the new DynamicForm (the name I assigned Vue.extend to) so it renders its own template.
I hope it helps as it was quite a pain to me!
What is alternative way of using package 'react-meteor-data' ? I'm using ES6 for writing react component. What would be best approach for writing meteor subscription in react component so that it will re-render the component if anything change on the server side.
What would be best approach for writing meteor subscription in react
component so that it will re-render the component if anything change
on the server side.
The best approach is using react-meteor-data. What is wrong with this package, that makes you think to not use it?
It even allows you to separate / decouple React Components from Meteor. This is really great because when you reach the point of having written some components that you want to reuse in another non-Meteor project you are free to go without greater hassle.
Of course you could write your own subscription container for react, but with this package you have all important stuff you need plus it is maintained and tested.
If you have trouble setting up a container for subscriptions, you may dig deeper into these tutorials:
https://guide.meteor.com/react.html
https://themeteorchef.com/tutorials/using-create-container
Using react-meteor-data, you first create a HOC container that subscribes to the data and passes it down as props to your component.
Container:
import { createContainer } from 'meteor/react-meteor-data';
export default FooContainer = createContainer(() => {
// Do all your reactive data access in this method.
// Note that this subscription will get cleaned up when your component is unmounted
var handle = Meteor.subscribe("todoList", this.props.id);
return {
currentUser: Meteor.user(),
listLoading: ! handle.ready(),
tasks: Tasks.find({listId: this.props.id}).fetch(),
};
}, Foo);
Component:
import FooContainer from './FooContainer';
class App extends React.Component {
render() {
return (
<div>
{this.props.currentUser.name}
</div>
)
}
}
export default FooContainer(App);
Check out React docs for Higher Order Components to understand how the container works.
I'm displeased with the formulation of the question. Feel encouraged to suggest an improvement. Also, please keep in mind that due to ignoyance (ignorance leading to annoyance), I might have flawed diagnostics of hte issue. Sorry about that.
In this answer it's suggested to use this.$store.xxx and it fails in my code because this is undefined. I strongly suspect something stupid being done by the author of the code (that would be me), so I'll present the schematics of my component layout.
The way it's intended is that I have a landing page index.js that creates two components - one for the visuals of the application and one for the storage of information. The visual App will consist of a navigation bar (and a rendering area later on). The navbar will dispatch commands to the store (and to the viewing area) rendering different *.vue files showing tables, lists etc.
So, how come I get to see the text this is undefined? Is my structure entirely flawed or am I just missing a small detail here and there?
index.js
import Vue from "vue"
import Store from "./vuex_app/store"
import App from "./vuex_modules/app.vue"
new Vue({ el: "#app-base", components: { App }, store: Store });
store.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
const state = { ... };
const mutations = { ... };
export default new Vuex.Store({ state, mutations });
app.vue
<template><div id="app">App component<navigation></navigation></div></template>
<script>
import navigation from "./navigation.vue"
export default { components: { navigation } }
</script>
navigation.vue
<template><div id="nav-bar"><p v-on:click="updateData">Update</p></div></template>
<script>
import { updateData } from "../vuex_app/actions";
export default {
vuex: {
actions: { updateData },
getters: { ... }
},
methods: {
updateData: () => {
console.log("this is " + this);
this.$store.dispatch("updateData");
}
}
}
</script>
actions.js
export const updateData = ({dispatch}, data) => {
console.log("invoked updateData");
dispatch("UPDATE_DATA", data);
};
Vue.js offers a pretty nice reactive type, minimalist JavaScript framework. Unfortunately, per this link there may be some unusual usage requirements. In this case,
Don’t use arrow functions on an instance property or callback (e.g.
vm.$watch('a', newVal => this.myMethod())). As arrow functions are
bound to the parent context, this will not be the Vue instance as
you’d expect and this.myMethod will be undefined.