Convert Vue2 component (factory) to Vue3 with render function - vue.js

I would like to convert an existing Vue2 component to Vue3 but I'm having problems. Basically what you see below is what I come up so far (with irrelevant code left out). I could share the Vue2 code as well but it does not look very different.
When running the app I get the error Component is missing template or render function. So what could be the issue here? Is it because there is possibly a different instance of Vue being defined with createApp?
I think the inner workings of the mounted and other hooks are not that important right now. What is puzzling me is that the render function is not even called. I checked that with a log statement.
import { createApp, h } from "vue";
const app = createApp({});
export default (Component, tag = "span") =>
app.component("adaptor", {
render() {
return h(tag, {
ref: "container",
props: this.$attrs,
});
},
data() {
return {
comp: null,
};
},
mounted() {
this.comp = new Component({
target: this.$refs.container,
props: this.$attrs,
});
},
updated() {
this.comp.$set(this.$attrs);
},
unmounted() {
this.comp.$destroy();
},
});
It basically provides a wrapper to render components of other frameworks in Vue. It is used like this in Vue2 at the moment:
import toVue from "../toVue";
[...]
{ components: { MyComp: toVue(MyOtherFrameworkComp) }
[...]

Related

Provide/inject axios response

I have one component in which I use axios to get data from API, it works and I can use this data in this component, but when I try to provide this data to another component, I dont get any data there.
Here is part of my code:
data() {
return {
theme: [],
};
},
provide() {
return {
theme: this.theme
}
},
methods: {
getTheme() {
axios
.get(here is my api url)
.then((response) => (this.theme = response.data.data));
},
},
mounted() {
this.getTheme();
},
and this is the second component:
<template>
<div class="project-wrapper">
<project-card
v-for="course in theme.courses"
:name="course.name"
:key="course.id"
></project-card>
</div>
</template>
<script>
import ProjectCard from "../components/ProjectCard.vue";
export default {
inject: ["theme"],
components: {
ProjectCard,
}
};
</script>
What is wrong with my code?
Second option in the link may help you
provide() {
return {
$theme: () => this.theme,
}
},
and
inject: ["$theme"],
computed: {
computedProperty() {
return this.$theme()
}
}
and
v-for="course in computedProperty.courses"
When you set provide to 'handle' theme it adds reactivity to the value of theme - i.e the empty array ([]).
If you modify the elements in this array, it will remain reactive - however if you replace the array then the reactivity is broken.
Instead of overwriting theme in the axios call, try adding the resulting data to it. For example:
getTheme() {
axios
.get(here is my api url)
.then((response) => (this.theme.push(...response.data.data));
}
You are passing theme to the child component as injected property.
See Vue.js Docs:
The provide and inject bindings are NOT reactive. This is intentional.
However, if you pass down an observed object, properties on that
object do remain reactive.
As inject bindings are not reactive, the changed value of theme will not be visible from inside of the child component (it will stay the same as if no axios call happened).
Solution 1
Pass the value to the child component as an observed object. It means that in in your getTheme() method you will not rewrite the whole property value (this.theme = ...) but only write into the object which is already stored in the property (this.theme.themeData = ...).
data() {
return {
theme: { },
};
},
provide() {
return {
theme: this.theme
}
},
methods: {
getTheme() {
axios
.get(here is my api url)
.then((response) => (this.theme.themeData = response.data.data));
},
},
mounted() {
this.getTheme();
}
Solution 2
Alternatively you can pass the value to the child component using classical props which are always reactive.

Can't mock module import in Cypress Vue component tests

I'm new to Cypress component testing, but I was able to set it up easily for my Vue project. I'm currently investigating if we should replace Jest with Cypress to test our Vue components and I love it so far, there is only one major feature I'm still missing: mocking modules. I first tried with cy.stub(), but it simply didn't work which could make sense since I'm not trying to mock the actual module in Node.js, but the module imported within Webpack.
To solve the issue, I tried to use rewiremock which is able to mock Webpack modules, but I'm getting an error when running the test:
I forked the examples from Cypress and set up Rewiremock in this commit. Not sure what I'm doing wrong to be honest.
I really need to find a way to solve it otherwise, we would simply stop considering Cypress and just stick to Jest. If using Rewiremock is not the way, how am I suppose to achieve this? Any help would be greatly appreciated.
If you are able to adjust the Vue component to make it more testable, the function can be mocked as a component property.
Webpack
When vue-loader processes HelloWorld.vue, it evaluates getColorOfFruits() and sets the data property, so to mock the function here, you need a webpack re-writer like rewiremock.
export default {
name: 'HelloWorld',
props: {
msg: String
},
data() {
return {
colorOfFruits: getColorOfFruits(), // during compile time
};
},
...
Vue created hook
If you initiialize colorOfFruits in the created() hook, you can stub the getColorOfFruits function after import but prior to mounting.
HelloWorld.vue
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<h2>{{ colorOfFruits.apple }}</h2>
<p>
...
</template>
<script lang="ts">
import { getColorOfFruits } from "#/helpers/fruit.js";
export default {
name: "HelloWorld",
getColorOfFruits, // add this function to the component for mocking
props: {
msg: String,
},
data() {
return {
colorOfFruits: {} // initialized empty here
};
},
created() {
this.colorOfFruits = this.$options.colorOfFruits; // reference function saved above
}
});
</script>
HelloWorld.spec.js
import { mount } from "#cypress/vue";
import HelloWorld from "./HelloWorld.vue";
it("mocks an apple", () => {
const getMockFruits = () => {
return {
apple: "green",
orange: "purple",
}
}
HelloWorld.getColorOfFruits = getMockFruits;
mount(HelloWorld, { // created() hook called as part of mount()
propsData: {
msg: "Hello Cypress!",
},
});
cy.get("h1").contains("Hello Cypress!");
cy.get("h2").contains('green')
});
We solved this by using webpack aliases in the Cypress webpack config to intercept the node_module dependency.
So something like...
// cypress/plugins/index.js
const path = require("path");
const { startDevServer } = require('#cypress/webpack-dev-server');
// your project's base webpack config
const webpackConfig = require('#vue/cli-service/webpack.config');
module.exports = (on, config) => {
on("dev-server:start", (options) => {
webpackConfig.resolve.alias["your-module"] = path.resolve(__dirname, "path/to/your-module-mock.js");
return startDevServer({
options,
webpackConfig,
})
});
}
// path/to/your-module-mock.js
let yourMock;
export function setupMockModule(...) {
yourMock = {
...
};
}
export default yourMock;
// your-test.spec.js
import { setupMock } from ".../your-module-mock.js"
describe("...", () => {
before(() => {
setupMock(...);
});
it("...", () => {
...
});
});

Access Vue3 global components

So I'm trying to convert this Vue2 project to Vue3(typescript).
It's registering components globally and accessing them to match against a value in my store, however when trying to implement this in Vue3 the components stay undefined.
import getComponentTypeForContent from "../api/getComponentTypeForContent";
import { mapState } from "vuex";
import { defineComponent } from "vue";
export default defineComponent({
name: "PageComponentSelector",
beforeCreate: function () {
console.log("CREATED PAGECOMPONENTSELECTOR");
},
computed: mapState({
model: (state) => state.epiDataModel.model,
modelLoaded: (state) => state.epiDataModel.modelLoaded,
}),
methods: {
getComponentTypeForPage(model) {
// this.$options.components will contain all globally registered components from main.js
return getComponentTypeForContent(model, this.$options.components);
// this.$options.components fetches all components for vue2 app
},
},
});
and registering components like this:
//Pages
import LoginPage from "./components/pages/Login.vue";
const appAdv = createApp(App);
//Register components
appAdv.component("LoginPage", LoginPage);
appAdv.use(store).use(router).mount("#appAdv");
Can't find (or searching badly) how to do this or similar in vue3 so I've come here hoping someone could help hehe
I also frequently use parent components in child components, but don't (or haven't) used the App.component method
In vue3 I usually use the provide/inject . method
Provide
const app = createApp(App);
app.provide('someVarName', someVar); // `Provide` a variable to all components here
Inject:
// In *any* component
const { inject } = Vue;
...
setup() {
const someVar = inject('someVarName'); // injecting variable in setup
return {someVar}
}

vue.js 2 single file component with dynamic template

I need a single file component to load its template via AJAX.
I search a while for a solution and found some hints about dynamic components.
I crafted a combination of a parent component which imports a child component and renders the child with a dynamic template.
Child component is this:
<template>
<div>placeholder</div>
</template>
<script>
import SomeOtherComponent from './some-other-component.vue';
export default {
name: 'child-component',
components: {
'some-other-component': SomeOtherComponent,
},
};
</script>
Parent component is this
<template>
<component v-if='componentTemplate' :is="dynamicComponent && {template: componentTemplate}"></component>
</template>
<script>
import Axios from 'axios';
import ChildComponent from './child-component.vue';
export default {
name: 'parent-component',
components: {
'child-component': ChildComponent,
},
data() {
return {
dynamicComponent: 'child-component',
componentTemplate: null,
};
},
created() {
const self = this;
this.fetchTemplate().done((htmlCode) => {
self.componentTemplate = htmlCode;
}).fail((error) => {
self.componentTemplate = '<div>error</div>';
});
},
methods: {
fetchTemplate() {
const formLoaded = $.Deferred();
const url = '/get-dynamic-template';
Axios.get(url).then((response) => {
formLoaded.resolve(response.data);
}).catch((error) => {
formLoaded.reject(error);
}).then(() => {
formLoaded.reject();
});
return formLoaded;
},
},
};
</script>
The dynamic template code fetched is this:
<div>
<h1>My dynamic template</h1>
<some-other-component></some-other-component>
</div>
In general the component gets its template as expected and binds to it.
But when there are other components used in this dynamic template (some-other-component) they are not recognized, even if they are correctly registered inside the child component and of course correctly named as 'some-other-component'.
I get this error: [Vue warn]: Unknown custom element: some-other-component - did you register the component correctly? For recursive components, make sure to provide the "name" option.
Do I miss something or is it some kind of issue/bug?
I answer my question myself, because I found an alternative solution after reading a little bit further here https://forum.vuejs.org/t/load-html-code-that-uses-some-vue-js-code-in-it-via-ajax-request/25006/3.
The problem in my code seems to be this logical expression :is="dynamicComponent && {template: componentTemplate}". I found this approach somewhere in the internet.
The original poster propably assumed that this causes the component "dynamicComponent" to be merged with {template: componentTemplate} which should override the template option only, leaving other component options as defined in the imported child-component.vue.
But it seems not to work as expected since && is a boolean operator and not a "object merge" operator. Please somebody prove me wrong, I am not a JavaScript expert after all.
Anyway the following approach works fine:
<template>
<component v-if='componentTemplate' :is="childComponent"></component>
</template>
<script>
import Axios from 'axios';
import SomeOtherComponent from "./some-other-component.vue";
export default {
name: 'parent-component',
components: {
'some-other-component': SomeOtherComponent,
},
data() {
return {
componentTemplate: null,
};
},
computed: {
childComponent() {
return {
template: this.componentTemplate,
components: this.$options.components,
};
},
},
created() {
const self = this;
this.fetchTemplate().done((htmlCode) => {
self.componentTemplate = htmlCode;
}).fail((error) => {
self.componentTemplate = '<div>error</div>';
});
},
methods: {
fetchTemplate() {
const formLoaded = $.Deferred();
const url = '/get-dynamic-template';
Axios.get(url).then((response) => {
formLoaded.resolve(response.data);
}).catch((error) => {
formLoaded.reject(error);
}).then(() => {
formLoaded.reject();
});
return formLoaded;
},
},
};
</script>

Update VueJs component on route change

Is there a way to re-render a component on route change? I'm using Vue Router 2.3.0, and I'm using the same component in multiple routes. It works fine the first time or if I navigate to a route that doesn't use the component and then go to one that does. I'm passing what's different in props like so
{
name: 'MainMap',
path: '/',
props: {
dataFile: 'all_resv.csv',
mapFile: 'contig_us.geo.json',
mapType: 'us'
},
folder: true,
component: Map
},
{
name: 'Arizona',
path: '/arizona',
props: {
dataFile: 'az.csv',
mapFile: 'az.counties.json',
mapType: 'state'
},
folder: true,
component: Map
}
Then I'm using the props to load a new map and new data, but the map stays the same as when it first loaded. I'm not sure what's going on.
The component looks like this:
data() {
return {
loading: true,
load: ''
}
},
props: ['dataFile', 'mapFile', 'mapType'],
watch: {
load: function() {
this.mounted();
}
},
mounted() {
let _this = this;
let svg = d3.select(this.$el);
d3.queue()
.defer(d3.json, `static/data/maps/${this.mapFile}`)
.defer(d3.csv, `static/data/stations/${this.dataFile}`)
.await(function(error, map, stations) {
// Build Map here
});
}
You may want to add a :key attribute to <router-view> like so:
<router-view :key="$route.fullPath"></router-view>
This way, Vue Router will reload the component once the path changes. Without the key, it won’t even notice that something has changed because the same component is being used (in your case, the Map component).
UPDATE --- 3 July, 2019
I found this thing on vue-router documentation, it's called In Component Guards. By the description of it, it really suits your needs (and mine actually). So the codes should be something like this.
export default () {
beforeRouteUpdate (to, from, next) {
// called when the route that renders this component has changed,
// but this component is reused in the new route.
// For example, for a route with dynamic params `/foo/:id`, when we
// navigate between `/foo/1` and `/foo/2`, the same `Foo` component instance
// will be reused, and this hook will be called when that happens.
// has access to `this` component instance.
const id = to.params.id
this.AJAXRequest(id)
next()
},
}
As you can see, I just add a next() function. Hope this helps you! Good luck!
Below is my older answer.
Only saved for the purpose of "progress"
My solution to this problem was to watch the $route property.
Which will ended up you getting two values, that is to and from.
watch: {
'$route'(to, from) {
const id = to.params.id
this.AJAXRequest(id)
}
},
The alternate solution to this question handles this situation in more cases.
First, you shouldn't really call mounted() yourself. Abstract the things you are doing in mounted into a method that you can call from mounted. Second, Vue will try to re-use components when it can, so your main issue is likely that mounted is only ever fired once. Instead, you might try using the updated or beforeUpdate lifecycle event.
const Map = {
data() {
return {
loading: true,
load: ''
}
},
props: ['dataFile', 'mapFile', 'mapType'],
methods:{
drawMap(){
console.log("do a bunch a d3 stuff")
}
},
updated(){
console.log('updated')
this.drawMap()
},
mounted() {
console.log('mounted')
this.drawMap()
}
}
Here's a little example, not drawing the d3 stuff, but showing how mounted and updated are fired when you swap routes. Pop open the console, and you will see mounted is only ever fired once.
you can use just this code:
watch: {
$route(to, from) {
// react to route changes...
}
}
Yes, I had the same problem and solved by following way;
ProductDetails.vue
data() {
return {
...
productId: this.$route.params.productId,
...
};
},
methods: {
...mapActions("products", ["fetchProduct"]),
...
},
created() {
this.fetchProduct(this.productId);
...
}
The fetchProduct function comes from Vuex store. When an another product is clicked, the route param is changed by productId but component is not re-rendered because created life cycle hook executes at initialization stage.
When I added just key on router-view on parent component app.vue file
app.vue
<router-view :key="this.$route.path"></router-view>
Now it works well for me. Hopefully this will help Vue developers!
I was having the same issue, but slightly different. I just added a watch on the prop and then re-initiated the fetch method on the prop change.
import { ref, watch } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import Page from './content/Page.vue';
import Post from './content/Post.vue';
const props = defineProps({ pageSlug: String });
const pageData = ref(false);
const pageBodyClass = ref('');
function getPostContent() {
let postRestEndPoint = '/wp-json/vuepress/v1/post/' + props.pageSlug;
fetch(postRestEndPoint, { method: 'GET', credentials: 'same-origin' })
.then(res => res.json())
.then(res => {
pageData.value = res;
})
.catch(err => console.log(err));
}
getPostContent();
watch(props, (curVal, oldVal) => {
getPostContent();
});
watch(pageData, (newVal, oldVal) => {
if (newVal.hasOwnProperty('data') === true && newVal.data.status === 404) {
pageData.value = false;
window.location.href = "/404";
}
});
router - index.js
{
path: "/:pageSlug",
name: "Page",
component: Page,
props: true,
},
{
path: "/product/:productSlug",
name: "Product",
component: Product,
},
{
path: "/404",
name: "404",
component: Error404,
}