Cannot find module './undefined' when updating a template ref in Vue - vue.js

Objective
Render components based on a dynamically changing reference (ref). User can perform a "search" feature that returns data and updates the reference. The updated reference should then in turn update the components which are rendered using v-for.
My Setup
I have an onMounted() lifecycle hook that makes an axios request and returns all the data into a reference.
onMounted(async () => {
const response = await axios.get('/api/subject/')
allSubjects.value = await response.data.data;
})
The reference:
const allSubjects = ref(null)
The template:
<OneSubject
v-for="subject in allSubjects"
:key="subject.id"
:subject="subject"
/>
Everything works fine so far...
Problem
When I make another request for my "search" feature, the axios request works fine and I am able to get a response with data (a new array of objects).
The problem occurs when trying to update my reference with this data like so:
async function search(searchInput) {
const response = await axios.get(`/api/subject/search/${searchInput}`)
console.log(response) // <-- RESPONSE HAS DATA
allSubjects.value = await response.data.data; // <-- CAUSES ERROR
}
The error that is thrown comes from the component that is rendered in the v-for:
I can verify that the reference was successfully updated with new data, but the problem seems to arise when rendering the component in the v-for?
Update
Here is the component that it is trying to render in the v-for.
<template>
<div class="subject-wrapper" v-bind:style="{ background: `rgba(0, 0, 0, .3) url('${imgSrc}') center / cover` }">
<div class="darken-bg"></div>
<div class="subject-name">{{ subject.name }}</div>
</div>
</template>
<script setup>
import { onMounted, computed } from 'vue'
const props = defineProps({ subject: Object })
const imgSrc = computed(() => require(`#/assets/images/subject/${props.subject.image}`))
</script>
I updated my component as follows:
<template>
<div class="subject-wrapper" v-bind:style="{ background: `rgba(0, 0, 0, .3) url('${imgSrc}') center / cover` }">
<div class="darken-bg"></div>
<div class="subject-name">{{ subject.name }}</div>
</div>
</template>
<script setup>
import { reactive, computed, onUpdated } from 'vue'
const props = defineProps({ subject: Object })
const subject = reactive(props.subject)
const imgSrc = computed(() => require(`#/assets/images/subject/${subject.image}`))
onUpdated(() => {
console.log('component subject prop:', props.subject)
console.log('subject reactive:', subject)
})
</script>
Afterwards, the search executes without the error, but it renders the wrong component. I've console logged the values:
Solution
I found out that the error was actually coming from my server response. As you can see in the image above, the prop only has 2 values. When I am rendering the component, I require an image property that is non existent, thus throwing the error.
I updated my endpoint to return all the properties in the document and it now works.
Thanks yoduh for the suggestions that helped me get to the bottom if this!

I can't explain the console.logs at the bottom of your question, but based on the child component code I believe there are two changes needed to fix the overall issue.
require shouldn't ever be used in code when using Vite since require is ESM only. The Vite documentation describes a better way to import dynamic assets using new URL with import.meta.url. One caveat is that you can't use # alias when constructing the URL due to Rollup limitations (Vite's bundler). Your imgSrc then should look like this:
const imgSrc = computed(
() => new URL(`../assets/images/subject/${subject.value.image}`, import.meta.url).href
);
(actual relative path might be different on your local machine)
Change from using reactive to toRef when creating subject. Since props is reactive and you want to pull out the individual (non-reactive) property, you would use toRef for the job specifically because it keeps subject and props.subject synced together.
const subject = toRef(props, 'subject');
I don't have your exact same setup but when I tested it locally with some mock data/local image files instead of an API it worked for me. If you still have problems let me know.

Related

Vue Vite + CosmicJS

I'm building a blog SPA website as a hobby, and trying to figure out what would be the best way to get the smallest latency to acquire posts from a database.
So far I've tried Wordpress, but its API at least at the very first initial request every time, with API cache enabled, takes about a second (800-1100ms) to load only a handful posts - 6 to be precise each with a picture and about 2-300 words, and this is only for testing.
So I'm looking around for other possible solutions to make the request faster but stay free of charge and came across Cosmic JS.
I installed the cosmicjs module, but getting all sorts of errors as I try to initiate the requests, based on their documentation which looks like the following:
<script>
const Cosmic = require('cosmicjs')
const api = Cosmic()
// Set these values, found in Bucket > Settings after logging in at https://app.cosmicjs.com/login
const bucket = api.bucket({
slug: "YOUR_BUCKET_SLUG",
read_key: "YOUR_BUCKET_READ_KEY"
})
</script>
First, you can't use require in Vite, so I've changed
this
const Cosmic = require('cosmicjs')
to this
import Cosmic from "cosmicjs"
But I'm still getting error:
ReferenceError: process is not defined
at node_modules/cosmicjs/dist/helpers/constants.js (cosmicjs.js?v=2a84de6d:1367:19)
at __require2 (chunk-NKHIPFFU.js?v=2a84de6d:15:50)
at node_modules/cosmicjs/dist/main.js (cosmicjs.js?v=2a84de6d:1387:21)
at __require2 (chunk-NKHIPFFU.js?v=2a84de6d:15:50)
at node_modules/cosmicjs/dist/index.js (cosmicjs.js?v=2a84de6d:3359:23)
at __require2 (chunk-NKHIPFFU.js?v=2a84de6d:15:50)
at cosmicjs.js?v=2a84de6d:3371:16ű
Can't figure out what to do next to even make this work, currently my code looks like this for the cosmic request part:
import Cosmic from "cosmicjs"
const api = Cosmic();
const bucket = api.bucket({
slug: "NOT-GOING-TO-SHOW-SORRY-AND-THX",
read_key: "NOT-GOING-TO-SHOW-SORRY-AND-THX",
});
const data = await bucket.objects
.find({
type: "posts", // Object Type slug
})
.props("title,slug,metadata") // response properties
.limit(10); // number of Objects to be returned
console.log(data)
Any idea might be a good help, thanks in advance
Figured it out:
So anyone trying to use in Vite ANY node module that has process as a function in any part in that module, should do the following
In your vite.config.ts or vite.config.js add the following
export default defineConfig({
// ...
define: {
'process.env': process.env
}
})
And instead of require
const Cosmic = require('cosmicjs')
always use import
import Cosmic from "cosmicjs"
Besides that, everything works the same as in other API requests, so eg. in my case, I'm API requesting posts from my cosmic js Bucket
<script setup lang="ts">
import { ref } from "vue";
import { onMounted } from "vue";
import moment from "moment";
import Cosmic from "cosmicjs"
const api = Cosmic();
const posts = ref([] as any);
const isLoading = ref(false);
const bucket = api.bucket({
slug: "c27229f0-9018-11ed-b853-65fa50acc7e7",
read_key: "G71yMNlvizQCtrvVyp9Z1AecQp8a4RTr5dl9uEGi6nst9FNQIW",
});
async function fetchData() {
isLoading.value = true
const data = await bucket.objects.find({
type: 'posts'
}).props('slug,title,content,metadata') // Limit the API response data by props
posts.value = data.objects
isLoading.value = false
console.log(posts)
}
onMounted(async () => {
fetchData();
});
</script>
and the iterate through them in my template
<template>
<ul v-if="!isLoading" class="blog-posts-ul" v-for="post in posts" :key="post.slug">
<div class="posts-card">
<a
><router-link
:to="/blog/ + post.slug"
key="post.id"
class="posts-permalink"
>
</router-link
></a>
<img
v-if="post.metadata.image != null"
class="post.metadata.hero"
:src="post.metadata.image.imgix_url"
:alt="post.title"
/>
<img v-else src="#/assets/logos/favicon-big.png" />
<div class="posts-date">
<p>
{{ moment(post.date).fromNow() + " " + "ago" }}
</p>
</div>
<div class="posts-text">
<h1 class="posts-title">{{ post.title }}</h1>
<p v-html="post.excerpt" class="posts-excerpt"></p>
</div>
</div>
</ul>
</template>
As a last sidenote, this works flawlessly compared to Wordpress API requests, I was using Wordpress for my backend CMS and even with API cache plugin enabled, it took around 800-1100ms to load the posts, now that time shrank to about 30ms for the API request of the text based data, and extra 30-40ms for each image (thumbnails for the posts).
As a request from a commenter, I'm including some description as to why Vite needs the change of vite.config.js
Basically, other vue instances do include by default the process.env in their configurations, Vite doesn't cause its generally a slower method for acquiring data - based on what I read, haven't tested this performance difference myself.
Vite is used instead of simply using Vue CLI, because its supposed to be even a bit faster then Vue CLI.
But, this is a small change.
"Why it is needed at all? I don't even see in your code above
calling any function named process"
Because it is this cosmicjs that is using it. I'm using a service that has a free tier, named Cosmic, and in their documentation it's advised to use their own node_module named comsicjs to fetch your data (posts, thumbnails etc etc).
This node module is what is using process, so when you are
importing it as I am as you can see above in my code:
import Cosmic from "cosmicjs"
And this is why you need to change your Vite's configuration file, so it can use process.
TLDR:
If you get an error in your Vite project with "process is not defined", that could be because a module you installed is using process, so simply add the following code to your vite.config.ts or vite.config.js
export default defineConfig({
....,
define: {
'process.env': process.env
}
})

How to use Vue in an existing typescript application without modules?

I'm trying to use Vue in "progressive enhancement" mode in an existing site, which already uses Typescript extensively.
The vue tutorial shows code like this:
<script type="module">
import { createApp, reactive, ref } from 'vue'
createApp({
setup() {
const counter = reactive({ count: 0 })
const message = ref('Hello World!')
return {
counter,
message
}
}
}).mount('#app')
</script>
<div id="app">
<h1>{{ message }}</h1>
<p>Count is: {{ counter.count }}</p>
</div>
But I don't want to have javascript in the html file, I want to relocate that code to a proper typescript file. But when I do this, I get compilation errors no matter how I try to arrange this.
If I don't qualify anything, I get errors like Error TS2304 (TS) Cannot find name 'createApp'. This makes sense because the whole point of Typescript is to type check and not let you reference unknown things.
So I try to import Vue like this:
import { Vue } from '../../node_modules/vue/dist/vue.global.js';
and change the call to Vue.createApp(...). This causes the browser error Cannot use import statement outside a module
If I try instead:
let { Vue } = require('../../node_modules/vue/dist/vue.global.js')
I get require is not defined.
I tried getting the vue.d.ts typings from https://github.com/vuejs/vue/blob/dev/types/vue.d.ts and referencing the file with /// <reference path="../../Scripts/vue.d.ts" />" />, but this just doesn't work, giving Error TS2304 (TS) Cannot find name 'Vue'.
I have tried many, many permutations of this but all of them run into one error or another.
So the question is: how can I retrofit Vue into an existing Typescript app without modules?

How to create dynamic components in vueJS

I am new to vueJS and am trying to load components dynamically. I searched on the web and experimented with many suggestions but am still not able to get it to work.
Scenario: I want to have a 'shell' component that acts as a container for swapping in and out other components based on user's selection. The file names of these components will be served up from a database
Here's the shell component:
<template>
<keep-alive>
<component :is='compName'></component>
</keep-alive>
</template>
<script>
props: ['vueFile'],
export default ({
data() {
compName: ()=> import('DefaultPage.vue')
},
watch: {
compName() {
return ()=> import(this.vueFile);
}
}
})
</script>
This does not work.
If I hard code the component file name, it works correctly... e.g.
return ()=> import('MyComponent.vue');
The moment I change the import statement to use a variable, it gives an error even though the variable contains the same string I hard code.
What am I doing wrong?
compName() {
const MyComponent = () => import("~/components/MyComponent.js");
}
You can see this post
https://vuedose.tips/dynamic-imports-in-vue-js-for-better-performance
You can put the components you want to add dynamically in a directory, for example: ./component, and try this
compName () {
return ()=> import(`./component/${this.vueFile}`);
}
The import() must contain at least some information about where the module is located.
https://webpack.js.org/api/module-methods/#dynamic-expressions-in-import

How can I load an nested component dynamically in Vuejs without hardcoding the url

I followed Markus Oberlehner for loading Vue components via http. We have a Vue component precompiled as a .js file hosted on a separate server. When the user navigates to this loader component below, the call to externalComponent() successfully fetches the .js file and renders the component on this page. That is great. This only works if we hardcode the url in the loader component.
We are building a plugin architecture into our site. We have written some single page Vue component files. Each of these files is a plugin. We precomiled these .vue files into .js files according to Markus Oberlehner's helpful tutorial here: https://github.com/maoberlehner/distributed-vue-applications-loading-components-via-http.
We also have a Vue component in our main site - let's call it the loader component - that fetches a .js file and renders it into a component using the externalComponent() method - demonstrated in Markus's tutorial. This works, but since the MyComponent is a constant defined outside of the loader component's data object, we cannot dynamically inject the plugin_id of from the vue router into the .js file's url.
If you're curious why our urls don't end in .js it is because we are passing a url to an endpoint in our server instead. This endpoint fetches the .js file and returns it to our client.
<template>
<div>
<MyComponent />
</div>
</template>
<script>
import util from "~/js/util.js";
let MyComponent = () =>
/* If we hadcode the url, the page renders no problemo.
*
* util.externalComponent("http://localhost:8081/api/plugins/167/code");
*/
/* However, we'd like to fetch the plugin_id from the Vue router and inject that into the argument
* as I've tried to achieve in the line of code below.
*
* The following code does not work because Vue apparently loads the MyComponent element in the DOM
* before executing the created() hook. We get the error "plugin_id is not defined."
*/
util.externalComponent(
"http://localhost:8081/api/plugins/" + plugin_id + "/code"
);
export default {
name: "plugin",
components: {
MyComponent
},
data() {
return {
plugin_id: null
};
},
created() {
/* This line does indeed populate the plugin_id data variable, although this happens after the
* page attempts to load MyComponent
*/
this.plugin_id = this.$route.params.plugin_id;
}
};
</script>
So, how can we modify this code to dynamically insert the plugin_id into the url?
This is a nuxt project by the way.
Update
Here is an approach that works the first time I load the page, but consecutive loads are still a problem. Specifically, the first component just loads again regardless of whatever the new plugin id may be.
But this looks like the right direction...
<template>
<div>
<component v-bind:is="component" v-if="loadedUrl"></component>
{{ plugin_id }}
</div>
</template>
<script>
import util from "~/js/util.js";
export default {
name: "plugin",
props: [],
data() {
return {
loadedUrl: false,
component: null
};
},
beforeRouteEnter(to, from, next) {
next(vm => {
vm.loadedUrl = false;
vm.component = () =>
util.externalComponent(
"http://localhost:8081/api/plugins/" + to.params.plugin_id + "/code"
);
vm.loadedUrl = true;
next();
});
},
beforeDestroy() {
//does not seem to help.
console.log("in beforeDestroy");
this.component = null;
}
};
</script>

Vue js component template not updating with data

I have a weird issue in some of my Vue js components, let me explain. I only render my component template after data has been initialised like so:
<template>
<div>
<div v-if='!isLoading'>
<!-- content -->
</div>
<div v-else>...</div>
</div>
</template>
In the created method of this component, I get some data from the store and set isLoading to false like so.
data() {
return {
variable: null,
isLoading: true,
}
},
created() {
this.variable = this.$store.getters['someModule/someGetter']
this.isLoading = false
}
Here's where the weird behaviour happens. Even though I updated the isLoading variable to false in the created method, the component template is not updating.
When I log the isLoading variable to the console at the end of the created method, it logs false, like i set it. But when I check the isLoading variable in the Vue js tools, it's still set to true...
Lets say this components is rendered in '/content'. This weird behaviour happens when I change routes from '/' to '/content'. When I refresh the app on the '/content' route, this doesn't happen. When I go from '/' to '/other-content' and then to '/content' it also doesn't happen.
Any ideas on why this happens would be greatly appreciated.
Thanks is advance and have a nice day!
There are subtle differences between mounted and created in your case since you want to manipulate the DOM, you should use mounted lifecycle hook.
This answer would expound on the differences between the two lifecycle methods.
This is a working example of what you're trying to do: https://codesandbox.io/s/blissful-field-kjufc?file=/src/App.vue
The interesting part of the code is here:
async created() {
const response = await fetch("https://jsonplaceholder.typicode.com/photos");
const json = await response.json();
console.log("done loading the data");
if (json) this.isLoading = false;
},
You can go to your network tab and select "slow 3G" to have an emulated slow connection. That way, you will see that the VueJS logo is not displayed until we have fetched all the 5000 photos.
If it's not helping, we definitely need more details, like vue devtools debugging or a reproduction.