Vue.js ReferenceError in event payload - vue.js

I have a user profile page with an image. I'm uploading the image to Firebase Storage, updating the user's record in Firestore to point at the image and displaying it. I have everything working in a single component.
Now I want to refactor to put the image uploading functionality into its own component with the idea that the new component will return the url of the uploaded file so that the parent component can then update Firestore. The benefit being that I can re-use the image upload component on other pages without the uploader needing to know why it's uploading images. The way I have architected this is as follows:
<template>
...
<v-btn
:disabled="!file"
class="primary"
small
#click.prevent="selected">Upload</v-btn>
...
</template>
export default {
name: "BaseImageUploader",
props: {
accept: {
type: String,
default: () => "image/png, image/jpeg, image/bmp"
},
placeholder: {
type: String,
default: () => "Click to select an image to upload"
},
label: {
type: String,
default: () => "Profile Picture"
}
},
data: () => ({
file: null
}),
methods: {
selected() {
const fileRef = storage.child("corporate-wellness-1/" + this.file.name);
fileRef
.put(this.file)
.then(uploadTaskSnapshot => uploadTaskSnapshot.ref.getDownloadURL())
.then(url => this.$root.$emit("base-image-uploader", url))
}
}
};
then the parent listens for the event and updates Firestore like so:
...
mounted() {
this.$root.$on(`base-image-uploader`, (event) => {
this.uploadImage(event)
});
},
...
uploadImage(url) {
this.$store
.dispatch("user/updateProfileImageUrl", url)
.then(() => console.log('image url updated in bio'))
}
The problem is I'm getting
ReferenceError: url is not defined
on the dispatch in the parent component.
The event only gets emitted after the url becomes available in the child component, so I don't understand why it's not available in the parent when the event handler is called.
So, I've two questions:
Why doesn't the code written work?
And more generally, is there a better way to architect it?

Related

Inheritance / shared action and getters in Pinia

I have a couple of Pinia stores that should share a set of actions and getters and I’m not quite sure how to effectively achieve that.
I’m building an app that lets users manage a number of different media (Books, Movies, TV Shows, etc). The way I’m currently thinking about it is to have a store for each media type e.g. BookStore, MovieStore etc. A lot of the getters and actions (e.g., count and deleteOne) are exactly the same between those different stores.
How do I achieve DRY here? The Pinia documentation has examples that mostly focus around reusing actions and getters inside other stores but I don’t think that quite solves my use case of inheriting a set of getters and setters outright.
Is my attempted inheritance approach here an anti-pattern?
This is achievable using plugins docs
Example Movies:
You have multiple stores using shared naming scheme for each state:
item: single entity item (single movie details)
collection: collection of items (collection of all movies)
each store will have the same CRUD actions with only the URL changing
getCollection: get list of items from API and set response as collection (https://url.com/movies)
getItem: get single item from API and set response as item (https://url.com/movies/id)
handleError: displays alert to the user with error information
Create plugin:
function BaseStorePlugin () {
return {
collection: [],
item: {},
getCollection: function (url) {
api.get(url)
.then((response) => {
this.collection = response.data;
})
.catch((error) => {
this.handleError(error);
});
},
getItem: function (url) {
api.get(url)
.then((response) => {
this.item = response.data;
})
.catch((error) => {
this.handleError(error);
});
},
handleError: function (error) {
window.alert(error);
},
};
}
Give plugin to Pinia:
const pinia = createPinia();
pinia.use(BaseStorePlugin);
Example movieStore.js (using shared action & state)
import { defineStore } from 'pinia';
import { api } from 'src/boot/axios';
export const useMovieStore = defineStore({
id: 'movie',
state: () => ({
movieSpecificStateObject: {},
}),
actions: {
movieSpecificAction (url) {
console.log(this.item);
api.get(url)
.then((response) => {
// handle response
})
.catch((error) => {
this.handleError(error);
});
},
},
});
Example usage in component
<template>
<div
v-for="movie in movieStore.collection"
:key="movie.id"
>
<div>
{{ movie.name }}
</div>
</div>
</template>
<script setup>
import { onMounted } from 'vue';
import { useMovieStore } from 'src/stores/movieStore.js';
const movieStore = useMovieStore();
onMounted(() => {
movieStore.readCollection('http://url.com/movies');
});
</script>
Edit: 1
if you pass the context into the plugin you have access to the store and options being passed into it, from this you could check the store id and only return for specific stores like below
function BaseStorePlugin (context) {
const allowedStores = ['movie', 'album'];
if (allowedStores.includes(context.store.$id)) {
return {
collection: [],
getCollection: function () {
const fakeCollection = Array.from({length: 10}, () => Math.floor(Math.random() * 40));
fakeCollection.forEach((item) => {
this.collection.push({
id: item,
name: `name${item}`
});
});
},
};
};
}
I have created a very basic example using 3 stores and the above check available on codesandbox here

How to upload multiple image in Vue.JS and axios (Vuex)

all day I’m trying to solve the problem with multi-upload images
In console log im see all the data but when I try to write this data to a variable im get empty array
If someone have info about solve this problem I'll be very grateful
This is my code in Vue component for upload images
<div class="card-body">
<form>
<vue-upload-multiple-image
#upload-success="uploadImageSuccess(id)"
#before-remove="beforeRemove"
#edit-image="editImage"
:data-images="images"
idUpload="myIdUpload"
editUpload="myIdEdit"
></vue-upload-multiple-image>
</form>
</div>
This is my data in Vue Component
data()
{
return{
images: [],
}
},
This is my function from upload images in Vue Component
uploadImageSuccess(id, formData, index, fileList) {
this.images = fileList;
console.log(formData)
console.log(index)
console.log(fileList)
this.addNewGallery({id, data: this.images});
},
This is my code from Vuex Store from upload images
async addNewGallery(ctx, {id, data})
{
return new Promise((resolve, reject) => {
axios({
url: '/gallery/' + id,
method: 'PUT',
data: data,
})
.then((resp) => {
console.log(resp)
})
.catch((error) => {
console.log(error)
reject(error)
})
})
}
This is my code from Console tab in browser
[{…}, ob: Observer]
0:
name: "IPhoneX.png"
path: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAC7gA"
highlight: 1
default: 1

load routes from api and import views dynamically

The most of my routes are protected and require permissions to access them. When the user signed in successfully my Navbar component makes an API call and retrieves a bunch of routes the user is able to access.
After that I add all the view files matching to the routes to the navbar.
This is an example code showing the process
<template>
<div>
<router-link
v-for="navItem in navItems"
:key="navItem.title"
:to="navItem.url"
>{{ navItem.title }}</router-link>
</div>
</template>
<script>
export default {
data: function() {
return {
navItems: []
};
},
created: async function() { // Setup the router here
this.navItems = [
// !!! API CALL !!!
{
title: "Dashboard",
url: "/Dashboard"
},
{
title: "Users",
url: "/users/Users"
},
{
title: "Groups",
url: "/groups/Groups"
}
];
const routes = await this.navItems.map(async navItem => {
const { url } = navItem;
return {
path: url,
component: await import(`../views${url}.vue`)
};
});
this.$router.addRoutes(routes);
}
};
</script>
Unfortunately I get this error
Uncaught (in promise) Error: [vue-router] "path" is required in a
route configuration.
but as you can see in the example code this attribute is set. I created an sample project here
https://codesandbox.io/s/vue-routing-example-i2znt
If you call this route
https://i2znt.codesandbox.io/#/Dashboard
I would expect the router to render the Dashboard.vue file.
the routes array that you build doesn't contains your routes objects.
It's an array of promises.
you should do something like
Promise.all(routes).then(resolvedRoutes => {
this.$router.addRoutes(resolvedRoutes)
})

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,
}

Vuex rendering data that is fetched from REST API

For such component
<template>
<div>
<router-link :to="{name:'section', params: { sectionId: firstSectionId }}">Start</router-link>
</div>
</template>
<script lang="ts">
import { mapActions } from "vuex"
export default {
mounted() {
this.getSectionId()
},
computed: {
firstSectionId() {
return this.$store.state.firstSectionId
}
},
methods: mapActions(["getSectionId"])
}
</script>
Store:
const store: any = new Vuex.Store({
state: {
firstSectionId: null
},
// actions,
// mutations
})
I have a web request in the getSectionId action and it asynchronously fetches data and calls a mutation that will fill firstSectionId in state. During the initial rendering firstSectionId is null and I get the warning that a required parameter is missing during rendering of router-link.
It is not a problem here to add v-if="firstSectionId". But in general what is the approach for fetching data from a server to be displayed? Currently all my components are checking if there is data present in the store before rendering, is it normal or is there a better way to wait for data to be loaded before rendering it?
One approach for asynchronously fetching data is to use promise in vuex store actions.
Vue.http.get(API_URL)
.then((response) => {
//use response object
})
.catch((error) => {
console.log(error.statusText)
});
To demonstrate that I make request to this route. You can see how response should looks like. Let's save response object in state.users array.
store.js
const store = new Vuex.Store({
state: {
users: []
},
mutations: {
FETCH_USERS(state, users) {
state.users = users
}
},
actions: {
fetchUsers({ commit }, { self }) {
Vue.http.get("https://jsonplaceholder.typicode.com/users")
.then((response) => {
commit("FETCH_USERS", response.body);
self.filterUsers();
})
.catch((error) => {
console.log(error.statusText)
});
}
}
})
export default store
You noticed that there is self.filteruser() method after commit. That is crucial moment. Before that we are committing a mutation, which is synchronous operation and we are sure that we will have our response in store.state that can be used in filterUsers() method (don't forget to pass self parm)
Users.vue
import store from "../store/store"
export default {
name: 'users',
created() {
this.$store.dispatch("fetchUsers", { self: this })
},
methods:{
filterUsers() {
//do something with users
console.log("Users--->",this.$store.state.users)
}
}
}
Better ways (ES6 & ES7)
ES6 Promises for asynchronous programming
//User.vue
created() {
this.$store.dispatch("fetchUser").then(() => {
console.log("This would be printed after dispatch!!")
})
}
//store.js
actions: {
fetchUser({ commit }) {
return new Promise((resolve, reject) => {
Vue.http.get("https://jsonplaceholder.typicode.com/users")
.then((response) => {
commit("FETCH_USERS", response.body);
resolve();
})
.catch((error) => {
console.log(error.statusText);
});
});
}
}
ES7: async/await
To get away from callback hell, and to improve asynchronous programming use async function, and you can await on a promise. Code looks much easier to follow (like it is synchronous), but code isn't readable for browsers so you'll need Babel transpiler to run it.
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // wait for actionA to finish
commit('gotOtherData', await getOtherData())
}
}
In my experience, you can skip a few checks if you preset the state with an empty value of the same type as the expected result (if you know what to expect, of course), e.g. if you have an array of items, start with [] instead of null as it won't break v-for directives, .length checks and similar data access attempts.
But generally, adding v-if is a very normal thing to do. There's a section about this in the vue-router documentation and checking whether properties exist or not is exactly what it suggests. Another possible solution it mentions is fetching data inside beforeRouteEnter guard, which assures you will always get to the component with your data already available.
Ultimately, both solutions are correct, and the decision between them is more of a UX/UI question.
I had similar requirements for locations and the google map api. I needed to fetch my locations from the API, load them in a list, and then use those in a map component to create the markers. I fetched the data in a Vuex action with axios, loaded that in my state with a mutation, and then used a getter to retrieve the resulting array in the mounted life cycle hook. This resulted in an empty array as mounted fired before the async action resolved.
I used store.subscribe to solve it this way:
<template>
<div class="google-map" :id="mapName"></div>
</template>
<script>
import GoogleMapsLoader from 'google-maps';
import { mapGetters } from 'vuex';
export default {
name: 'google-map',
props: ['name'],
computed: {
...mapGetters({
locations: 'locations/locations',
}),
},
data() {
return {
mapName: `${this.name}-map`,
};
},
mounted() {
this.$store.subscribe((mutation, state) => {
if (mutation.type === 'locations/SAVE_LOCATIONS') {
GoogleMapsLoader.KEY = 'myKey';
GoogleMapsLoader.load((google) => {
/* eslint-disable no-new */
const map = new google.maps.Map(document.getElementById('locations-map'));
// loop through locations and add markers to map and set map boundaries
const bounds = new google.maps.LatLngBounds();
// I access the resulting locations array via state.module.property
state.locations.locations.forEach((location) => {
new google.maps.Marker({
position: {
lat: location.latitude,
lng: location.longitude,
},
map,
});
bounds.extend({
lat: location.latitude,
lng: location.longitude,
});
});
map.fitBounds(bounds);
});
}
});
},
};