Vue.js variable not updating correctly - javascript

I am having trouble a variable in vue.js.
Scenario - when a user logs in, I want to set a loggedIn variable to true and set it to false when the user logs out. My code:
index.js:
export default {
state: {
loggedIn : false
},
login() {
var self = this;
var creds = {
username: 'username',
password: 'password'
}
$.ajax({
url: '/login',
type: 'POST',
data: creds,
success: function(response) {
self.state.loggedIn = true;
alert('Logged in!!');
},
error: function() {
alert('Error logging in!');
}
});
},
}
App.vue:
import auth from './index.js';
module.exports = {
data: function () {
return {
loggedIn: auth.state.loggedIn,
}
},
watch: {
loggedIn: function () {
alert('loggedIn value has changed!!!');
}
},
}
As you can see, in App.vue, my loggedIn variable depends on what's imported from index.js. However, it doesn't appear that loggedIn in App.vue is reactive to loggedIn in index.js.
Does anyone know what I might be doing wrong?
Thanks in advance!

In order to make some data reactive, you must set it as the data of a component.
Since auth.state.loggedIn holds a primitive (a Boolean), assigning its value to data.loggedIn simply copies it over to data.loggedIn.
So while data.loggedIn is reactive, auth.state.loggedIn is not. The two are simply never linked up.
The only way to make this work is to assign the whole state object to your data:
module.exports = {
data () {
return {
auth: auth.state,
}
},
watch: {
'auth.loggedIn' () {
alert('loggedIn value has changed!!!');
}
}
};

Related

Child is not updated when boolean prop is changed

I have the following components:
Parent:
<template>
<Child path="instance.json"
v-bind:authenticated="authenticated"
v-bind:authenticator="authenticator"
/>
</template>
<script>
import { getAuthenticator } from '../auth';
export default {
data() {
return {
authenticated: false,
authenticator: null
};
},
beforeMount: async function () {
this.authenticator = getAuthenticator()
this.checkAccess();
},
methods: {
checkAccess() {
this.authenticated = this.authenticator.isAuthenticated();
},
async login() {
this.checkAccess();
await this.authenticator.signIn();
this.checkAccess();
}
}
};
</script>
Child:
<template>
<div id="swagger-ui"></div>
</template>
<script>
import swagger from "swagger-ui-dist";
import "swagger-ui-dist/swagger-ui.css";
export default {
props: ["path", "authenticated", "authenticator"],
mounted: async function() {
if (this.authenticated) {
let token = (await this.authenticator.getToken()).accessToken;
const ui = swagger.SwaggerUIBundle({
url: this.path,
dom_id: "#swagger-ui",
onComplete: function() {
ui.preauthorizeApiKey("token", token);
}
});
} else {
const ui = swagger.SwaggerUIBundle({
url: this.path,
dom_id: "#swagger-ui"
});
}
}
};
</script>
In the parent component, when the login method is called, the authenticated variable changes to true. Since authenticated is passed as a prop to the Child component, I'd expect the Child to be refreshed whenever authenticated is changed. However, the Child does not refresh.
I think that the problem might be caused by the fact that I am not using authenticated in the template of the child at all. Instead, I'm using it only in the mounted hook. In my case, I have no use for authenticated in the template.
I tried two solutions:
calling this.$forceUpdate() in the login method of Parent - that didn't work at all (nothing changed)
Adding :key to the Child, and changing the key each time the login is called - this works, however, it's a bit hacky. I'd like to understand how to do that properly.
what you need is to use a watcher.
Actually, your code is only run once (when de component is mounted), not at each prop change.
<template>
<div id="swagger-ui"></div>
</template>
<script>
import swagger from 'swagger-ui-dist';
import 'swagger-ui-dist/swagger-ui.css';
export default {
props: {
path: {
type: String,
default: '',
},
authenticated: {
type: Boolean,
default: false,
},
authenticator: {
type: Object,
default: () => {},
},
},
watch: {
async authenticated(newValue) {
await this.updateSwagger(newValue);
},
},
async mounted() {
await this.updateSwagger(this.authenticated);
}
methods: {
async updateSwagger(authenticated) {
if (authenticated) {
const token = (await this.authenticator.getToken()).accessToken;
const ui = swagger.SwaggerUIBundle({
url: this.path,
dom_id: '#swagger-ui',
onComplete: function () {
ui.preauthorizeApiKey('token', token);
},
});
} else {
const ui = swagger.SwaggerUIBundle({
url: this.path,
dom_id: '#swagger-ui',
});
}
},
},
};
</script>
It's fine that you're not using it in the template, the issue is that you only check authenticated in the child's mounted hook, which only runs once (and is false at that time).
You should use a watch to track changes to the authenticated prop instead of mounted:
watch: {
authenticated: {
handler(newValue, oldValue) {
this.setUi();
},
immediate: true // Run the watch when `authenticated` is first set, too
}
}
That will call a setUi method every time authenticated changes:
methods: {
async setUi() {
if (this.authenticated) {
let token = (await this.authenticator.getToken()).accessToken;
const ui = swagger.SwaggerUIBundle({
url: this.path,
dom_id: "#swagger-ui",
onComplete: function() {
ui.preauthorizeApiKey("token", token);
}
});
} else {
const ui = swagger.SwaggerUIBundle({
url: this.path,
dom_id: "#swagger-ui"
});
}
}
}

How to redirect user to home page if user is authentic i.e ID or PASSWORD is correct in vue.js using VUEX STORE state?

I am setting user authenticity in VUEX Store state if user id and password are valid. But how would I access this in my signIn.vue to redirect?
If user is valid then set VUEX store state i.e isUserAuthentic to true
For this I've used computed properties. to get user state i.e isUserAuthentic and work around in hasUserSignedIn in computed properties. I'm checking if user is authentic then redirect and return undefined from this computed properties. So that I can use this in HTML and doesn't affect HTML because I'm returning undefined from that computed properties.
It is working but that's not perfect/best practices.
SignIn.vue
<template>
<section>
<div class="form-wrapper">
//--------------------------------------USING COMPUTED PROPERTIES
{{ hasUserSignedIn }}
<input v-model="email"/>
<input v-model="password"/>
<button class="btn-signin" #click="submit()">
sign in
</button>
</div>
</section>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
data() {
email: '',
password: '',
},
methods: {
submit() {
if (this.email && this.password) {
this.$store.dispatch('signInUser', { email: this.email, password: this.password });
}
},
redirectTohome() {
this.$router.push({ path: '/home' });
},
},
computed: {
...mapGetters(['isUserAuthentic']),
//-----------------------------------------WORKAROUND COMPUTED PROPERTIES
hasUserSignedIn() {
if (this.isUserAuthentic) {
this.redirectTohome();
}
return undefined;
},
},
};
</script>
VUEX signin.js
import Vue from 'vue';
import axios from 'axios';
// const URL = 'http://localhost:3000';
const state = {
signInLoading: false,
isUserAuthentic: false,
};
const getters = {
isSignInLoading: (signInState) => signInState.signInLoading,
isUserAuthentic: (signInState) => signInState.isUserAuthentic,
};
const mutations = {
SET_SIGNIN_LOADING_STATUS(signInState, status) {
signInState.signInLoading = status;
},
SET_USER_AUTHENTICITY(signInState, isAuthentic) {
signInState.isUserAuthentic = isAuthentic;
},
};
const actions = {
async signInUser({ commit }, payload) {
// SET LOADING STATUS TRUE
commit('SET_SIGNIN_LOADING_STATUS', true);
try {
// AUTHORIZE USER WITH AXIOS
const response = await axios.post(`${URL}/api/v1/user/signin`, payload);
// IF USER IS AUTHENTIC, SET AUTHENTIC STATUS TO TRUE
if (response.status === 200) {
commit('SET_USER_AUTHENTICITY', true);
}
} catch (e) {
// SEND TOAST NOTIFICATION TO USER FOR INVALID REQUESTS
if (e.response && e.response.data.message) {
Vue.$toast(e.response.data.message, {
type: 'info',
timeout: 8000,
});
} else {
Vue.$toast('Something went wrong, Please try again.', {
type: 'error',
timeout: 8000,
});
}
}
// SET LOADING STATUS FALSE
commit('SET_SIGNIN_LOADING_STATUS', false);
},
};
export default {
state,
getters,
mutations,
actions,
};
You are pretty much spot on. The only thing I feel you should do is that you can wait for the promise of the store dispatch('signInUser') to resolve and check the state of isUserAuthentic. Something like this:
SignIn.vue
<script>
import { mapGetters } from 'vuex';
export default {
data() {
email: '',
password: '',
},
methods: {
async submit() { // use the 'async' keyword so that you can use await
if (this.email && this.password) {
await this.$store.dispatch('signInUser', { email: this.email, password: this.password }); // wait for the signInUser action to complete
if (this.isUserAuthentic) {
this.redirectTohome();
}
}
},
redirectTohome() {
this.$router.push({ path: '/home' });
}
},
},
computed: {
...mapGetters(['isUserAuthentic']), // mapState will be better here since you don't alter the state
},
};
</script>
<template>
...
{{ isUserAuthentic }}
...
</template>
Also, as an aside, I feel you should fire a commit i.e commit('SET_USER_AUTHENTICITY', false); when there's a failure. That's missing from your code.
Since you are returning the state as is, and not altering it, mapState will be better than mapGetters in your case.
I read the docs and I think WATCHERS are much better than COMPUTED in this particular case.
In this approach we don't have to use {{ hasUserSignedIn }} in HTML. Just watch the property and when it is true redirect to home.
watch: {
isUserAuthentic(val) {
if (val) {
this.redirectTohome();
}
},
},
If someone have better solution, You are more than welcome

How to make objects properties reactive when they rely on an external object?

tldr:
User is a global object.
If I change show value, the component will update instantly, that's OK.
What I want to gain is like "When User.isLoggedIn() becomes false, the Log out element must hide. When it becomes true, the element must show and Login/Signup must hide." In my app, this goal would transform into another, "When I'm redirecting from login,signup, orsignout pages, these properties(and state of the button) must be updated."
Toolbar.Vue.
Script:
<script>
export default {
data() {
return {
items: [
{title: 'Questions', to: '/questions', show: true},
{title: 'Ask question', to: '/askQuestion', show: true},
{title: 'Categories', to: '/categories', show: true},
// only F5.
{title: 'Login/Signup', to: '/login', show: !User.isLoggedIn()},
{title: 'Log out', to: '/logout', show: User.isLoggedIn()},
]
}
},
}
Piece of markup:
<router-link
v-for="item in items"
:key="item.title"
:to="item.to"
v-if="item.show"
>
You know, I'm trying to do the 'logout thing' with vue. I have that Toolbar component with router-link to Logout component.
Note: I don't import User class in my component directly, but I do it in my app.js. like this:
import User from './helpers/AppUser';
window.User = User;
So, I think that everybody has access to the right User. Furthermore, this class is just a grouped couple of methods. Main method is retrieve().
In my Logout component there's the code:
beforeMount() {
User.logout();
router.push('questions')
// window.location = '/questions'
}
So, when I go logout, all is fine (I'm returning to questions page), but my Log out button is still here.
User.isLoggedIn() works properly (When I F5 my page, all is fine).
I also mentioned that if I change show value, the component will update instantly, that's OK.
That try also doesn't work:
{title: 'Login/Signup', to: '/login', show: ()=> !this.isLoggedIn},
{title: 'Log out', to: '/logout', show: ()=> this.isLoggedIn},
],
}
},
computed:{
isLoggedIn: function() {
return User.isLoggedIn();
},
My temp solution is using window.location = '/questions' instead of vue-router.
Maybe I need some watchers, or add User in my global Vue... I don't know.
Update: User class.
/**
* Class-helper for managing user login/signup part. Also represents the user.
*/
class AppUser {
constructor() {
this.storageKey = 'appUser';
}
/**
* retrieves user data from localStorage. if none stored, returns null
* #return {object}|{null}
*/
retrieve() {
let data = {};
try {
data = JSON.parse(localStorage.getItem(this.storageKey));
} catch (e) {
console.log(e);
} finally {
// console.log(data)
}
return data;
}
/**
* clears localStorageEntry
*/
clear() {
localStorage.removeItem(this.storageKey);
}
/**
* #return boolean
*/
hasId() {
let data = this.retrieve();
return data === null ? false : data.hasOwnProperty('id');
// if id then return true
}
/**
* #return boolean
*/
hasToken() {
let data = this.retrieve();
return data === null ? false : data.hasOwnProperty('jwt');
// if token then return true
}
isLoggedIn() {
// console.log('in user.isLoggedIn')
// try {
return this.hasToken() && this.hasId();
// }
// catch (e) {
// console.log(e);
// }
}
}
export default AppUser = new AppUser();
You can replace the original method of the User at "created" phase.
return {
data () {
return {
items: [
{ title: 'Questions', to: '/questions', show: true },
{ title: 'Ask question', to: '/askQuestion', show: true },
{ title: 'Categories', to: '/categories', show: true },
{ title: 'Login/Signup', to: '/login', show: !User.isLoggedIn() },
{ title: 'Log out', to: '/logout', show: User.isLoggedIn() },
]
}
},
created () {
let app = this;
let items = app.items;
let loginUser = User.login.bind(User);
let logoutUser = User.logout.bind(User);
User.login = () => {
// modify the data
items[3].show = false;
items[4].show = true;
// do login
loginUser();
};
User.logout = () => {
// modify the data
items[3].show = true;
items[4].show = false;
// do logout
logoutUser();
};
}
};

Cancel/Abort Axios Post on Vue Component

I have got a Vue Component which has a list of values, when you select these values this changed the selected array, which in tern is posted to an endpoint.
I have an issue if the user spam clicks these values, as an individual post is created for each change, I want it so that if the user selects another item then the currently pending post is cancelled, so then the new value is posted and updates the endpoint with both the selected items.
However i'm having an issue with aborting the current axios request, I have provided the code below. There are no errors, the request simply doesn't cancel.
export default {
props: {
endpoint: {
default: '',
type: String
},
parameters: {
default: null,
type: Object
}
},
data: () => ({
loaded: false,
selected: [],
save: [],
data: [],
cancel: undefined
}),
methods: {
update() {
const self = this;
let params = this.parameters;
params.data = this.selected;
this.$root.$emit('saving', {
id: this._uid,
saving: true
});
if (self.cancel !== undefined) {
console.log('cancel');
this.cancel();
}
window.axios.post(this.endpoint + '/save', params, {
cancelToken: new window.axios.CancelToken(function executor(c) {
self.cancel = c;
})
}).then(() => {
this.$nextTick(() => {
this.loaded = true;
this.$root.$emit('saving', {
id: this._uid,
saving: false
});
});
}).catch(function (thrown) {
if (window.axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
}
});
}
}
}
I have got a global instance of Axios created on my Vue Application.

Using vue-resource in a plugin

I am trying to put all my vue-resource requests into a separate component/plugin for easy re-use and no duplication, but being new to Vue I am not sure if this is the correct approach.
It seems a little clumsy.
I want to be able to define all my resources in my plugin as well as be able to initialize new sessions and setup the Auth header globally. The following works but I am sure there is a better/cleaner way.
Would appreciate any guidance/advice on the better approach here.
Thanks
Plugin - DataService.js
Created an Init function to setup the global Auth header and then some other properties to store paths and options.
const DataService = {
init : function(newsession) {
this.session = newsession;
Vue.http.headers.common['Authorization'] = newsession;
},
userpath : '/api/users/',
options: {emulateJSON: true},
};
DataService.install = function (Vue, options) {
Vue.prototype.$getUser = function (userid) {
return this.$http.get(DataService.userpath + userid);
}
Vue.prototype.$saveUser = function (user) {
return this.$http.post(DataService.userpath + user.id,{data: user},DataService.options);
}
}
Vue Instance - account.js
Note: I am first initializing my DataService to pass in the new session as well as call it as a Plugin (.use).
DataService.init(session_id);
Vue.use(DataService);
new Vue({
el: '#app',
data: {
user: { id: null, username: null},
session: { },
processing: false,
alert: "",
warning: "",
},
computed: {
},
mounted: function() {
this.loadUser();
},
methods: {
loadUser: function () {
this.$getUser(userid).then(function(response) {
// get body data
var res = response.body;
this.user = res.data;
this.session = res.session;
}, function(response) {
console.error('Error loading user',response);
});
},
saveUser: function () {
this.$saveUser(this.user).then( function(response) {
// success callback
console.log('Saved user',response);
}, function(response){
// error callback
console.error('Error Saving user',response);
});
}
}
});

Categories