Related
I'm building a small e-commerce store with an admin panel for myself.
I use Firebase firestore as my backend to store all the user's data.
I have a root 'users' collection with a document for every single registered user and everything else each user has is branching out of the user doc.
Here are firestore commands i perform so you understand the structure better.
db.collection('users').doc(userId).collection('categories').doc(subCategoryId)...
db.collection('users').doc(userId).collection('subcategories').doc(subCategoryId)...
I use Vuex so every time i need to change something on my firestore (update a product category, remove a category etc.), i dispatch an appropriate action.
The first thing any of those actions does is to go ahead and dispatch another action from auth.js that gets the userId.
The problem is that if the action in question should run in a mounted() lifecycle hook, then it fails to grab the userId.
In EditCategory.vue updateCategory action works perfectly well because SubmitHandler() is triggered on click event but in Categories.vue the fetchCategories does not work and spit out an error:
[Vue warn]: Error in mounted hook (Promise/async): "FirebaseError: [code=invalid-argument]: Function CollectionReference.doc() requires its first argument to be of type non-empty string, but it was: null"
Function CollectionReference.doc() requires its first argument to be of type non-empty string, but it was: null
Which, as far as i understand it, basically tells me that fetchCategories() action's firestore query could not be performed because the userId was not recieved.
After two days of moving stuff around i noticed that errors only occur if i refresh the page. If i switch to other tab and back on without refreshing, then fetchCategories() from Categories.vue mounted() hook works. Placing the code in to created() hook gives the same result.
I think that there is some fundamental thing i am missing about asynchronous code and lifecycle hooks.
Categories.vue component
<template>
<div class="category-main">
<section>
<div class="section-cols" v-if="!loading">
<EditCategory
v-on:updatedCategory="updatedCategory"
v-bind:categories="categories"
v-bind:key="categories.length + updateCount"
/>
</div>
</section>
</div>
</template>
<script>
import EditCategory from '#/components/admin/EditCategory.vue'
export default {
name: 'AdminCategories',
components: {
EditCategory,
},
data: () => ({
updateCount: 0,
loading: true,
categories: [],
}),
async mounted() {
this.categories = await this.$store.dispatch('fetchCategories');// FAILS!
this.loading = false;
},
methods: {
addNewCategory(category) {
this.categories.push(category);
},
updatedCategory(category) {
const catIndex = this.categories.findIndex(c => c.id === category.id);
this.categories[catIndex].title = category.title;
this.categories[catIndex].path = category.path;
this.updateCount++;
}
}
}
</script>
category.js store file
import firebase, { firestore } from "firebase/app";
import db from '../../fb';
export default {
actions: {
async getUserId() {
const user = firebase.auth().currentUser;
return user ? user.uid : null;
},
export default {
state: {
test: 10,
categories: [],
subCategories: [],
currentCategory: '',
},
mutations: {
setCategories(state, payload){
state.categories = payload;
},
},
actions: {
async fetchCategories({commit, dispatch}) {
try {
const userId = await dispatch('getUserId');
const categoryArr = [];
await db.collection('users').doc(userId).collection('categories').get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
categoryArr.push({ id: doc.id, ...doc.data() })
});
})
commit('setCategories', categoryArr);
return categoryArr;
} catch (err) { throw err; }
},
async updateCategory({commit, dispatch}, {title, path, id}) {
try {
const userId = await dispatch('getUserId');
console.log('[category.js] updateCategory', userId);
await db.collection('users').doc(userId).collection('categories').doc(id).update({
title,
path
})
commit('rememberCurrentCategory', id);
return;
} catch (err) {throw err;}
}
},
}
auth.js store file
import firebase, { firestore } from "firebase/app";
import db from '../../fb';
export default {
actions: {
...async login(), async register(), async logout()
async getUserId() {
const user = firebase.auth().currentUser;
return user ? user.uid : null;
},
},
}
index.js store file
import Vue from 'vue'
import Vuex from 'vuex'
import auth from './auth'
import products from './products'
import info from './info'
import category from './category'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
auth, products, info, category,
}
})
EditCategory.vue
export default {
name: 'EditCategory',
data: () => ({
select: null,
title: '',
path: '',
current: null
}),
props: {
categories: {
type: Array,
required: true
}
},
methods: {
async submitHandler() {
if (this.$v.invalid){
this.$v.$touch()
return;
}
try {
const categoryData = {
id : this.current,
title: this.title,
path: this.path
};
await this.$store.dispatch('updateCategory', categoryData);// WORKS!
this.$emit('updatedCategory', categoryData);
} catch (err) { throw err; }
},
},
//takes current category id from store getter
computed: {
categoryFromState() {
return this.$store.state.currentCategory;
}
},
created() {
console.log('[EditCategory.vue'], currentCategory);
},
mounted(){
this.select = M.FormSelect.init(this.$refs.select);
M.updateTextFields();
},
destroyed() {
if (this.select && this.select.destroy) {
this.select.destroy;
}
}
}
</script>
First of all, it's just a small detail, but you don't need need to make your 'getUserId' action async, since it does not use the 'await' keyword. So can simplify this :
async getUserId() {
const user = firebase.auth().currentUser;
return user ? user.uid : null;
}
const userId = await dispatch('getUserId')
into this :
getUserId() {
const user = firebase.auth().currentUser;
return user ? user.uid : null;
}
const userId = dispatch('getUserId')
Coming back to your id that seems to be undefined, the problem here is that your 'mounted' event is probably triggered before the 'login' can be completed.
How to solve this case ? Actually, there are a lot of different ways to approch this. What I suggest in your case is to use a middleware (or a 'route guard'). This guard can make you are verified user before accessing some routes (and eventually restrict the access or redirect depending on the user privileges). In this way, you can make sure that your user is defined before accessing the route.
This video is 4 years old so it is not up to date with the last versions of Firebas. But I suggest The Net Ninja tutorial about Vue Route Guards with Firebase if you want to learn more about this topic.
Accepted answer actually pointed me to the correct direction.
In my case i had to make a route guard for child routes.
router.vue
import Vue from 'vue'
import Router from 'vue-router'
import firebase from 'firebase/app';
Vue.use(Router);
const router = new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'home',
meta: {layout: 'main-layout'},
component: () => import('./views/main/Home.vue')
},
{
path: '/bouquets',
name: 'bouquets',
meta: {layout: 'main-layout'},
component: () => import('./views/main/Bouquets.vue')
},
{
path: '/sets',
name: 'sets',
meta: {layout: 'main-layout'},
component: () => import('./views/main/Sets.vue')
},
{
path: '/cart',
name: 'cart',
meta: {layout: 'main-layout'},
component: () => import('./views/main/Cart.vue')
},
{
path: '/login',
name: 'login',
meta: {layout: 'empty-layout'},
component: () => import('./views/empty/Login.vue')
},
{
path: '/register',
name: 'register',
meta: {layout: 'empty-layout'},
component: () => import('./views/empty/Register.vue')
},
{
path: '/admin',
name: 'admin',
meta: {layout: 'admin-layout', auth: true},
component: () => import('./views/admin/Home.vue'),
children: [
{
path: 'categories',
name: 'adminCategories',
meta: {layout: 'admin-layout', auth: true},
component: () => import('./views/admin/Categories'),
},
{
path: 'subcategories',
name: 'adminSubcategories',
meta: {layout: 'admin-layout', auth: true},
component: () => import('./views/admin/SubCategories'),
},
{
path: 'products',
name: 'adminProducts',
meta: {layout: 'admin-layout', auth: true},
component: () => import('./views/admin/Products'),
},
]
},
{
path: '/checkout',
name: 'checkout',
meta: {layout: 'main-layout'},
component: () => import('./views/main/Checkout.vue')
},
{
path: '/:subcategory',
name: 'subcategory',
meta: {layout: 'main-layout'},
props: true,
params: true,
component: () => import('./views/main/Subcategory.vue')
},
]
})
router.beforeEach((to, from, next) => {
//if currentUser exists then user is logged in
const currentUser = firebase.auth().currentUser;
//does a route has auth == true
const requireAuth = to.matched.some(record => record.meta.auth);
//if auth is required but user is not authentificated than redirect to login
if (requireAuth && !currentUser) {
// next('/login?message=login');
next('login')
} else {
next();
}
})
export default router;
category.js fetchCategories() action
async fetchCategories({commit, dispatch}) {
const userId = await dispatch('getUserId')
try {
const categoryArr = [];
await db.collection('users').doc(userId).collection('categories').get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
categoryArr.push({ id: doc.id, ...doc.data() })
});
})
commit('setCategories', categoryArr);
return categoryArr;
} catch (err) { throw err; }
},
Hi everybody i'm trying to make login page and redirect to home page ('/')
When i'm logging i haven't errors in console i can see the error using vue devtools
ERROR VUE DEV TOOL
End of navigation
/login
02:27:19.124
guard:afterEach
failure:Avoided redundant navigation to current location: "/login".
status:❌
from:/login
fullPath:"/login"
path:"/login"
query:Object (empty)
hash:""
name:"login"
params:Object (empty)
matched:Array[1]
meta:Object (empty)
redirectedFrom:undefined
href:"/login"
to:/login
fullPath:"/login"
hash:""
query:Object (empty)
name:"login"
path:"/login"
params:Object (empty)
matched:Array[1]
meta:Object (empty)
redirectedFrom:Object
href:"/login"
this is my login's method
methods:{
async submitForm(user){
const userForm=new FormData();
userForm.append("username", this.username);
userForm.append("password", this.password);
await this.$store.dispatch("auth/login", userForm).then(
()=>{
const user = localStorage.getItem('user')
console.log(user) //to check if i logged, in console get undefined but if try localStorage.getItem('user') i got the user.
this.$router.push('/')
}),
(error)=>{
console.log(error)
}
}
}
ROUTES
const router = createRouter({
history: createWebHistory(),
scrollBehavior() {
return { top: 0 }
},
routes,
})
{
path: '/',
name: 'dashboard',
component: () => import('#/views/Dashboard.vue'),
children: [
{
path: '',
name: 'home',
component: () => import('#/views/dashboard/Home.vue'),
},
....
router.beforeEach((to, from, next) => {
const publicPages = ['/login'];
const authRequired = !publicPages.includes(to.path);
const loggedIn = localStorage.getItem('user');
// trying to access a restricted page + not logged in
// redirect to login page
if (authRequired && !loggedIn) {
next('/login');
} else {
next();
}
});
auth.service
class AuthService {
login(user) {
let dator={
access_token: '',
user:{}
}
console.log('AUTHSERVICE-->\n'+user)
return axios
.post(API_URL + 'login/access-token', user)
.then(response => {
console.log(response.data.access_token)
if (response.data.access_token) {
dator.access_token=response.data.access_token
localStorage.setItem('token', JSON.stringify(dator.access_token))
axios.get(API_URL + 'users/me/', { headers: authHeader() })
.then(response =>{
localStorage.setItem('user', JSON.stringify(response.data))
dator.user=response.data
})
}
return dator;
});
}
auth.module VUEX
import AuthService from '../services/auth.service';
const token = JSON.parse(localStorage.getItem('token'));
const user = JSON.parse(localStorage.getItem('user'));
const initialState = token && user
? { status: { loggedIn: true }, token,user }
: { status: { loggedIn: false }, token:null, user: null };
export const auth = {
namespaced: true,
state: initialState,
actions: {
login({ commit }, userForm) {
console.log(userForm)
return AuthService.login(userForm).then(
datologin => {
console.log('datologin',datologin)
commit('loginSuccess', datologin);
return Promise.resolve(datologin);
},
error => {
commit('loginFailure');
return Promise.reject(error);
}
);
},
if after login i force the '/' in the browser the page work. So i don't know where is my bad.
Sorry for noob error. The problem was in second request with axios so i refactor this part and now work
async asyncLogin(user){
let uservuex={
access_token: '',
user:{}
}
try{
const token = await axios.post(API_URL + 'login/access-token', user)
uservuex.access_token = await token.data.access_token
localStorage.setItem('token', JSON.stringify(uservuex.access_token))
const me = await axios.get(API_URL + 'users/me/', { headers: authHeader() })
uservuex.user= await me.data
localStorage.setItem('user', JSON.stringify(uservuex.user))
}catch(error){
console.log(error)
}
return uservuex
}
I'm new to Vue.js and I have created one simple form for the user and storing data using API.
On submit I'm calling this function:
setup(props, { emit }) {
const blankData = {
customer: '',
template: '',
rate: '',
property_from: '',
property_to: '',
move_date: '',
num_days: '',
token: '',
details: '',
customer_options: [],
template_options: [],
rate_options: [],
property_from_options: [],
property_to_options: [],
}
const userData = ref(JSON.parse(JSON.stringify(blankData)))
const resetuserData = () => {
userData.value = JSON.parse(JSON.stringify(blankData))
}
const toast = useToast()
const onSubmit = () => {
store.dispatch('app-user/addUser', userData.value)
.then(
response => {
if (response.status === 1) {
this.$router.push({ name: 'edit-user', params: { id: 10 } })
}
toast({
component: ToastificationContent,
props: {
title: response.message,
icon: response.toastIcon,
variant: response.toastVariant,
},
})
},
error => {
console.log(error)
},
)
}
const {
refFormObserver,
getValidationState,
resetForm,
} = formValidation(resetuserData)
return {
userData,
onSubmit,
refFormObserver,
getValidationState,
resetForm,
}
},
And trying to redirect the user to the edit page after user creation but I'm getting this error and not redirecting:
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'router')
I have tried with this stackoverflow answer but getting same error:
const onSubmit = () => {
const self = this
store.dispatch('app-user/addUser', userData.value)
.then(
response => {
if (response.status === 1) {
self.$router.push({ name: 'edit-user', params: { id: 10 } })
}
},
error => {
console.log(error)
},
)
}
Any idea what I'm doing wrong in my code?
You're using Vue 3 and a setup function; there is no this in a setup function.
See Accessing the Router and current Route inside setup.
Untested, but probably something like this will work:
setup() {
const router = useRouter()
const onSubmit = () => {
// ... code omitted ...
router.push({ name: 'edit-user', params: { id: 10 } })
}
return {
onSetup,
// other stuff...
}
}
I think this might be help
router with composition api
https://next.router.vuejs.org/guide/advanced/composition-api.html
Context: I am trying to get place data via the place_id on the beforeEnter() route guard. Essentially, I want the data to load when someone enters the url exactly www.example.com/place/{place_id}. Currently, everything works directly when I use my autocomplete input and then enter the route but it does not work when I directly access the url from a fresh tab. I believe the issue is because google has not been created yet.
Question: How can I access PlacesService() using the beforeEnter() route guard ?
Error: Uncaught (in promise) ReferenceError: google is not defined
Example Code:
In one of my store modules:
const module = {
state: {
selectedPlace: {}
},
actions: {
fetchPlace ({ commit }, params) {
return new Promise((resolve) => {
let request = {
placeId: params,
fields: ['name', 'rating', 'formatted_phone_number', 'geometry', 'place_id', 'website', 'review', 'user_ratings_total', 'photo', 'vicinity', 'price_level']
}
let service = new google.maps.places.PlacesService(document.createElement('div'))
service.getDetails(request, function (place, status) {
if (status === 'OK') {
commit('SET_SELECTION', place)
resolve()
}
})
})
},
},
mutations: {
SET_SELECTION: (state, selection) => {
state.selectedPlace = selection
}
}
}
export default module
In my store.js:
import Vue from 'vue'
import Vuex from 'vuex'
import placeModule from './modules/place-module'
import * as VueGoogleMaps from 'vue2-google-maps'
Vue.use(Vuex)
// gmaps
Vue.use(VueGoogleMaps, {
load: {
key: process.env.VUE_APP_GMAP_KEY,
libraries: 'geometry,drawing,places'
}
})
export default new Vuex.Store({
modules: {
placeModule: placeModule
}
})
in my router:
import store from '../state/store'
export default [
{
path: '/',
name: 'Home',
components: {
default: () => import('#/components/Home/HomeDefault.vue')
}
},
{
path: '/place/:id',
name: 'PlaceProfile',
components: {
default: () => import('#/components/PlaceProfile/PlaceProfileDefault.vue')
},
beforeEnter (to, from, next) {
store.dispatch('fetchPlace', to.params.id).then(() => {
if (store.state.placeModule.selectedPlace === undefined) {
next({ name: 'NotFound' })
} else {
next()
}
})
}
}
]
What I've tried:
- Changing new google.maps.places.PlacesService to new window.new google.maps.places.PlacesService
- Using beforeRouteEnter() rather than beforeEnter() as the navigation guard
- Changing google.maps... to gmapApi.google.maps... and gmapApi.maps...
- Screaming into the abyss
- Questioning every decision I've ever made
EDIT: I've also tried the this.$gmapApiPromiseLazy() proposed in the wiki here
The plugin adds a mixin providing this.$gmapApiPromiseLazy to Vue instances (components) only but you're in luck... it also adds the same method to Vue statically
Source code
Vue.mixin({
created () {
this.$gmapApiPromiseLazy = gmapApiPromiseLazy
}
})
Vue.$gmapApiPromiseLazy = gmapApiPromiseLazy
So all you need to do in your store or router is use
import Vue from 'vue'
// snip...
Vue.$gmapApiPromiseLazy().then(() => {
let service = new google.maps.places....
})
I am writing the angular (Karma-Jasmine) test cases and I want to navigate between the pages. How to navigate between pages using karma-Jasmine.
1) Test a component in which navigation is used: navigate method should be called when you do an action (assertion like toHaveBeenCalled OR toHaveBeenCalledWith)
it('should redirect the user to the users page after saving', () => {
let router = TestBed.get(Router);
let spy = spyOn(router, 'navigate');
component.save();
expect(spy).toHaveBeenCalledWith(['users'])
});
2) Also test your routes that proper component will be used
app.routes.spec.ts
import { routes } from './app.routes'
it('should contain a route for users', () => {
expect(routes).toContain({path: 'users', component: UserComponent})
});
3) You can use useValue for testing different activatedRouteParams (just configure then and pass to providers, see example).
Component ts file example:
ngOnInit() {
this.contextType = this.route.snapshot.paramMap.get('contextType');
this.contextId = this.route.snapshot.paramMap.get('contextId');
this.contextFriendlyId = this.route.snapshot.paramMap.get('contextFriendlyId');
}
Spec file (configureTestData is a function that allows you to pass different configurable values, in my case activatedRouteParams)
export function configureTestData(activatedRouteParams) {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SetUpComponent],
imports: [RouterTestingModule],
providers: [
{
provide: ActivatedRoute, useValue: activatedRouteParams
}
]
});
}));
}
describe('SetUp Component:Folder ', () => {
let component: SetUpComponent;
let fixture: ComponentFixture<SetUpComponent>;
configureTestData({
snapshot: {
paramMap: convertToParamMap({
contextType: 'Folder',
contextId: 'FX6C3F2EDE-DB25-BC3D-0F16-D984753C9D2E',
contextFriendlyId: 'FX17373'
})
}
});
beforeEach(() => {
fixture = TestBed.createComponent(SetUpComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create set up component for folder', () => {
expect(component).toBeTruthy();
});
it('should create alert with required properties', () => {
expect(component.contextType).toEqual('Folder);
.... etc
});
});
4) router-outlet and routerLink
Template file:
<nav>
<a routerLink="todos"></a>
</nav>
<router-outlet></router-outlet>
beforeEach(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule.withRoutes([])],
declarations: [AppComponent]
});
});
it('should have router outlet', () => {
let de = fixture.debugElement.query(By.directive(RouterOutlet));
expect(de).not.toBeNull();
});
it('should have a link to todos page', () => {
let debugElements = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref));
let index = debugElements.findIndex(de => de.properties['href'] === '/todos');
expect(index).toBeGreaterThan(-1);
});
5) Stub for ActivatedRoute where we can push params
component.ts
ngOnInit() {
this.route.params.subscribe(p => {
if (p['id'] === 0) {
this.router.navigate(['not-found']);
}
});
}
Spec file:
class RouterStub {
navigate(params) {}
}
class ActivatedRouteStub {
private subject = new Subject();
get params () {
return this.subject.asObservable();
}
push(value) {
this.subject.next(value);
}
}
describe('Navigation Testing', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [],
providers: [
{provide: Router, useClass: RouterStub},
{provide: ActivatedRoute, useClass: ActivatedRouteStub}
]
});
});
it('should navigate to invalid page when invalid params passed', () => {
let router = TestBed.get(Router);
let spy = spyOn(router, 'navigate');
let route: ActivatedRouteStub = TestBed.get(ActivatedRoute);
route.push({id: 0});
expect(spy).toHaveBeenCalledWith(['not-found'])
});
});