I have login component which has the following method:
login() {
this.$v.loginValidationGroup.$touch();
if (this.$v.loginValidationGroup.$error) {
return;
}
this.setLogsInfo();
userService.login(this.email, this.password, this.twoFactorAuthCode, this.rememberMe, this.userOs, this.userIp, this.userAgent, this.browserName)
.then(authenticationToken => {
if(authenticationToken === "2FactorAuthRequired") {
this.is2FAuthEnabled = true;
}
else {
this.$store.dispatch('login', authenticationToken);
this.$router.push('/Home');
}
})
.catch(error => {
if (error instanceof AuthenticationError && error.errorType === AuthErrorType.WRONG_CREDENTIALS) {
this.loginError = 'wrongLoginCredentials';
} else if (error instanceof ValidationError) {
this.loginError = 'invalidLoginCredentials';
} else {
this.loginError = 'unknownLoginError';
}
this.$v.$reset();
});
},
After login the user is redirected to the Home component.
On my Home component I have made a modal that contains a welcome message:
<template>
<div class="modal v-model="visible">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Welcome</h5>
</div>
<div class="modal-body">
some text....
</div>
</div>
</div>
</div>
</template>
Is there any way I can tell the application to set the v-model "visible" to true when routing from the Login component to the Home component?
Mind you I ONLY want the v-model be set to true when entering the page from the Login compoment,not any other component.
Basically, you can make use of in-component navigation guard called "beforeRouteEnter". In it, you have access to the route which the router navigated from. You check if the from route is the login route, then set the visible variable through vm component instance provided by next function (remember in this navigation guard you don't have access to this component instance)
Home component:
data(){
return {
visible: false
}
},
beforeRouteEnter(to, from, next) {
next((vm) => {
if (from.path === "/login") {
vm.visible = true;
}
});
},
In your Home.vue component, you can set a router watcher which would have to and from params to check the previous and current routes.
Here is an example-
Home.vue
watch: {
$route(to, from) {
if(from.name == "Login") {
this.visible = true;
}
}
}
Related
I need help to complete my code.
This is what have done.
I am fetching options from API, so I have defined the initial state as
empty.
Once I have a response from API, I update the state of options.
My form is displayed once I have a response from API.
Now using v-bind I am binding the form.
Where I need help.
I need to watch for the changes in form. If the values of form elements are different from the state of the API response, I would like to enable the submit button.
When the save button is clicked, I need to filter the options that were changed & submit that form data to my pinia action called updateOptions.
Note: API handles post data in this way. Example: enable_quick_view: true
Thank you in advance.
options.js pinia store
import { defineStore } from 'pinia'
import Axios from 'axios';
import axios from 'axios';
const BASE_API_URL = adfy_wp_locolizer.api_url;
export const useOptionsStore = defineStore({
id: 'Options',
state: () => ({
allData: {},
options: {
enable_quick_view: null, // boolean
quick_view_btn_label: "", // string
quick_view_btn_position: "", // string
},
newOptions: {}, // If required, holds the new options to be saved.
message: "", // Holds the message to be displayed to the user.
isLoading: true,
isSaving: false,
needSave: false,
errors: [],
}),
getters: {
// ⚡️ Return state of the options.
loading: (state) => {
return state.isLoading;
},
},
actions: {
// ⚡️ Use Axios to get options from api.
fetchOptions() {
Axios.get(BASE_API_URL + 'get_options')
.then(res => {
this.alldata = res.data.settings;
let settings = res.data.settings_values;
/*
* Set options state.
*/
this.options.enable_quick_view = JSON.parse(
settings.enable_quick_view
);
this.options.quick_view_btn_label =
settings.quick_view_btn_label;
this.options.quick_view_btn_position = settings.quick_view_btn_position;
/*
* End!
*/
this.isLoading = false;
})
.catch(err => {
this.errors = err;
console.log(err);
})
.finally(() => {
// Do nothing for now.
});
},
// ⚡️ Update options using Axios.
updateOptions() {
this.isSaving = true;
axios.post(BASE_API_URL + 'update_options', payload)
.then(res => {
this.needSave = false;
this.isSaving = false;
this.message = "Options saved successfully!";
})
.catch(err => {
this.errors = err;
console.log(err);
this.message = "Error saving options!";
})
}
},
});
Option.vue component
<script setup>
import { onMounted, watch } from "vue";
import { storeToRefs } from "pinia";
import { Check, Close } from "#element-plus/icons-vue";
import Loading from "../Loading.vue";
import { useOptionsStore } from "../../stores/options";
let store = useOptionsStore();
let { needSave, loading, options, newOptions } = storeToRefs(store);
watch(
options,
(state) => {
console.log(state);
// Assign the option to the newOptions.
},
{ deep: true, immediate: false }
);
onMounted(() => {
store.fetchOptions();
});
</script>
<template>
<Loading v-if="loading" />
<form
v-else
id="ui-settings-form"
class="ui-form"
#submit="store.updateOptions()"
>
<h3 class="option-box-title">General</h3>
<div class="ui-options">
<div class="ui-option-columns option-box">
<div class="ui-col left">
<div class="label">
<p class="option-label">Enable quick view</p>
<p class="option-description">
Once enabled, it will be visible in product catalog.
</p>
</div>
</div>
<div class="ui-col right">
<div class="input">
<el-switch
v-model="options.enable_quick_view"
size="large"
inline-prompt
:active-icon="Check"
:inactive-icon="Close"
/>
</div>
</div>
</div>
</div>
<!-- // ui-options -->
<div class="ui-options">
<div class="ui-option-columns option-box">
<div class="ui-col left">
<div class="label">
<p class="option-label">Button label</p>
</div>
</div>
<div class="ui-col right">
<div class="input">
<el-input
v-model="options.quick_view_btn_label"
size="large"
placeholder="Quick view"
/>
</div>
</div>
</div>
</div>
<!-- // ui-options -->
<button type="submit" class="ui-button" :disabled="needSave == true">
Save
</button>
</form>
</template>
<style lang="css" scoped>
.el-checkbox {
--el-checkbox-font-weight: normal;
}
.el-select-dropdown__item.selected {
font-weight: normal;
}
</style>
In the watch function you can compare the new and old values. But you shuld change it to:
watch(options, (newValue, oldValue) => {
console.log(oldValue, newValue);
// compare objects
}, {deep: true, immediate: false};
Now you can compare the old with the new object. I think search on google can help you with that.
Hope this helps.
I have a sidebar that you can see below:
<template>
<section>
<div class="sidebar">
<router-link v-for="(element, index) in sidebar" :key="index" :to="{ name: routes[index] }" :class='{active : (index==currentIndex) }'>{{ element }}</router-link>
</div>
<div class="sidebar-content">
<div v-if="currentIndex === 0">
Profile
</div>
<div v-if="currentIndex === 1">
Meine Tickets
</div>
</div>
</section>
</template>
<script>
export default {
mounted() {
EventBus.$on(GENERAL_APP_CONSTANTS.Events.CheckAuthentication, () => {
this.authenticated = authHelper.validAuthentication();
});
console.log()
this.checkRouter();
},
data(){
return {
currentIndex:0,
isActive: false,
sidebar: ["Profile", "Meine Tickets"],
routes: ["profile", "my-tickets"],
authenticated: authHelper.validAuthentication(),
}
},
computed: {
getUser() {
return this.$store.state.user;
},
},
methods: {
changeSidebar(index) {
this.object = this.sidebar[index].products;
this.currentIndex=index;
},
checkRouter() {
let router = this.$router.currentRoute.name;
console.log(router);
if(router == 'profile') {
this.currentIndex = 0;
} else if(router == 'my-tickets') {
this.currentIndex = 1;
}
},
},
}
</script>
So when the link is clicked in the sidebar, the route is being changed to 'http://.../my-account/profile' or 'http://.../my-account/my-tickets'. But the problem is currentIndex doesn't change therefore, the content doesn't change and also I cannot add active class into the links. So how do you think I can change the currentIndex, according to the routes. Should I fire an event, could you help me with this also because I dont know how to do it in Vue. I tried to write a function like checkRouter() but it didn't work out. Why do you think it is happening? All solutions will be appreciated.
So if I understand correctly, you want currentIndex to be a value that's based on the current active route? You could create it as a computed property:
currentIndex: function(){
let route = this.$router.currentRoute.name;
if(router == 'profile') {
return 0;
} else if(router == 'my-tickets') {
return 1;
}
}
I think you could leverage Vue's reactivity a lot more than you are doing now, there's no need for multiple copies of the same element, you can just have the properties be reactive.
<div class="sidebar-content">
{{ sidebar[currentIndex] }}
</div>
Also, you might consider having object be a computed property, something like this:
computed: {
getUser() {
return this.$store.state.user;
},
object() {
return this.sidebar[currentIndex].products;
}
},
Just use this.$route inside of any component template. Docs .You can do it simple without your custom logic checkRouter() currentIndex. See simple example:
<div class="sidebar-content">
<div v-if="$route.name === 'profile'">
Profile
</div>
<div v-if="$route.name === 'my-tickets'">
Meine Tickets
</div>
</div>
In a recent web app we have a lot of forms with the same submit structure:
Disable the form and submit button based on an isSubmitting variable
Validate the input fields (we're using Yup)
If validation fails: Set isSubmitting back to false + set and show validationErrors on the input fields
If validation succeed: Send post request with form data to api
Show general error if api is down or returns an error
I've tried to something using the composition api in vue 3.
Login.vue
<template>
<div class="min-h-full flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div class="sm:mx-auto sm:w-full sm:max-w-md">
<h1 class="text-3xl text-center text-gray-900">{{ t('sign_in_account', 1) }}</h1>
</div>
<div class="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div class="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<form #submit.prevent="handleSubmit">
<fieldset :disabled="isSubmitting" class="space-y-6">
<MessageBox v-if="errors.general" :title="errors.general" :messages="errors.messages" />
<Input :label="t('email', 1)" type="text" id="email" v-model="user.email" :error="errors.email" />
<Password :label="t('password', 1)" type="password" id="password" v-model="user.password" :error="errors.password" />
<div class="text-sm text-right">
<router-link class="font-medium text-indigo-600 hover:text-indigo-500" :to="forgotPassword">{{ t('forgot_password', 1) }}</router-link>
</div>
<SubmitButton class="w-full" :label="t('sign_in', 1)" :submittingLabel="t('sign_in_loader', 1)" :isSubmitting="isSubmitting" />
</fieldset>
</form>
</div>
</div>
</div>
</template>
<script>
import { ref } from 'vue';
import { useStore } from 'vuex';
import { useI18n } from 'vue-i18n';
import useForm from '#/use/useForm';
import { validateEmail, LoginValidationSchema } from '#/utils/validators';
export default {
setup() {
const store = useStore();
const { t } = useI18n({ useScope: 'global' });
const user = ref({
email: '',
password: '',
});
const { handleSubmit, isSubmitting, errors } = useForm(user, LoginValidationSchema, handleLogin);
async function handleLogin(values) {
try {
return await store.dispatch('auth/login', values);
} catch (error) {
if (error.response) {
console.log(error.reponse);
if (error.response.status == 422) {
errors.value = {
general: `${t('unable_to_login', 1)}<br /> ${t('fix_and_retry', 1)}`,
messages: Object.values(error.response.data.errors).flat(),
};
} else if (error.response.data.message) {
errors.value = {
general: error.response.data.message,
};
} else {
errors.value = {
general: `${t('unknown_error', 1)}<br /> ${t('please_try_agin', 1)}`,
};
}
} else if (error.request) {
console.log(error.request);
errors.value = {
general: `${t('unknown_error', 1)}<br /> ${t('please_try_agin', 1)}`,
};
} else {
errors.value = {
general: `${t('unknown_error', 1)}<br /> ${t('please_try_agin', 1)}`,
};
}
return;
}
}
return { t, user, handleSubmit, isSubmitting, errors };
},
computed: {
forgotPassword() {
return validateEmail(this.user.email) ? { name: 'forgotPassword', query: { email: this.user.email } } : { name: 'forgotPassword' };
},
},
};
</script>
useForm.js
import { ref, watch } from 'vue';
export default function useForm(initialValues, validationSchema, callback) {
let values = ref(initialValues);
let isSubmitting = ref(false);
let errors = ref({});
async function handleSubmit() {
try {
errors.value = {};
await validationSchema.validate(values.value, { abortEarly: false });
isSubmitting.value = true;
} catch (err) {
console.log('In the catch');
isSubmitting.value = false;
err.inner.forEach((error) => {
errors.value = { ...errors.value, [error.path]: error.message };
});
}
}
watch(isSubmitting, () => {
if (Object.keys(errors.value).length === 0 && isSubmitting.value) {
callback(values);
isSubmitting.value = false;
} else {
isSubmitting.value = false;
}
});
return { handleSubmit, isSubmitting, errors };
}
This is somehow working but I'm missing two things. In useForm I want to wait till the callback is done (succeed or failed) to set isSubmitting back to false. Is a promise a good way to do this of is there a better way? Secondly I want a reusable way to handle the errors in Login.vue. Any suggestion how to handle this?
Regarding your first question - try..catch statements have a third statement called finally which always executes after the try statement block has completed.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try...catch
To answer your second question - promises are a great way of handling async logic, including your case when the API you're sending the request to returns an error response, and you can then decide how you're going to handle the UX in such scenario.
I'm not quite clear on what you mean by handle the errors in Login.vue in a reusable way, but perhaps you could simple pass in an empty array prop to your useForm called formErrors and have your useForm.js emit an update:modelValue event to get two way binding.
In vuex store I have this mutations which receives msg from one component and is to show/hide prompt message at another component (Like You are logged in propmpt after successful login) :
setPromptMsg: function (state, msg) {
state.showPromptMsg = true;
state.promptMsg = msg;
function sleep (time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
sleep(3000).then(() => {
store.showPromptMsg = false;
state.promptMsg = '';
console.log('show message set to false');
});
},
In the compoenet, I receive showPromptMsg from the store as a computed property:
computed: {
showPromptMsg () {
return this.$store.state.showPromptMsg;
},
promptMsg () {
return this.$store.state.promptMsg;
}
}
The show/hide prompt message in the template:
<div v-show="showPromptMsg">
<div class="text-center" >
<strong> {{promptMsg}} </strong>
</div>
</div>
The problem is that when the prompt is timedout, i.e. showPromptMsg is set to false at the store, the change is not reflected into the component, so the notification box does not disappear.
I'm wondering what is the idiomatic way to resolve this problem?
The code is setting
store.showPromptMsg = false;
I expect you want
state.showPromptMsg = false;
In your NotificationBarComponent.vue template:
<div>
<div class="text-center" >
<strong> {{ message }} </strong>
</div>
</div>
In your NotificationBarComponent.vue component definition add a prop to pass custom message to display and on mounted start the timeout to hide the notification:
export.default {
props: ['message'],
mounted() {
window.setTimeout(() => {
this.$store.commit('handleMessageState', false)
}, 3000);
}
};
in your store create a property to manage the notification display isNotificationBarDisplayed: false
handleMessageState(state, payload) {
state.isNotificationBarDisplayed = payload;
}
anywhere you want to use it:
<notification-bar-component v-show="isNotificationBarDisplayed" message="Some message here"></notification-bar-component>
computed: {
isNotificationBarDisplayed () {
return this.$store.state.isNotificationBarDisplayed;
},
}
I have a notifications component that has some notifications item child components that are fed from an array in the parent component. The child component has the ability to update and delete itself. It can mark itself as read when clicked. It will make a request to the server with Axios and then change a button icon to close (fa-close). Which works fine. Now it can delete itself. When clicked it will send a delete request to the server, and when successful emit an event to the parent component to delete it from the array with splice. Now it works fine but the issue I'm having is that the new icon that I changed still remains for the next component (next item in the array). And that bugs me because I can't seem to find a way to make it display the initial icon which was initialize with the component. here's some code if that can help NotificationsItem.vue <template>
<li class="list-group-item list-group-item-info">
<button class="pull-right"
title="#lang('text.notifications.markAsRead')"
#click="markAsReadOrDestroy">
<i class="fa" :class="iconClass" v-show="!loading"></i>
<i class="fa fa-spinner fa-spin fa-lg fa-fw" v-show="loading"></i>
</button>
<!-- {{ notification.data }} -->
I'm the index {{ index}} and the ID is {{notification.id}}
<span class="hljs-tag"></<span class="hljs-name">li</span>></span>
</template>
<script>
export default {
props: ['notification', 'index'],
data() {
return {
loading: false,
icon: 'check',
markedAsRead: false,
}
},
computed: {
iconClass() {
return 'fa-' + this.icon;
}
},
methods: {
markAsReadOrDestroy() {
if (this.markedAsRead) {
this.destroy();
} else {
this.markAsRead();
}
},
markAsRead() {
let vm = this;
this.loading = true;
this.$http.patch('/notifications/markasread/' + this.notification.id)
.then(function(response) {
console.log(response);
vm.loading = false
vm.markedAsRead = true
vm.icon = 'close'
})
.catch(function(error) {
console.log(error);
vm.loading = false;
});
},
destroy() {
let vm = this;
this.loading = true;
this.$http.delete('/notifications/' + this.notification.id)
.then(function(response) {
console.log(response);
vm.loading = false;
vm.$emit('deleted', vm.index);
})
.catch(function(error) {
console.log(error);
vm.loading = false;
});
}
},
mounted() {
console.log('Notifications Item mounted.')
}
}
</script>
NotificationsList.vue <template>
<div class="list-group">
<notifications-item
v-for="(notification, index) in notifications"
:notification="notification"
#deleted="remove"
:index="index">
{{ notification.data['text'] }}
</notifications-item>
</div>
</template>
<script>
export default {
data() {
return {
notifications: notifications.data,
}
},
methods: {
remove(index) {
console.log(index);
this.notifications.splice(index, 1);
}
},
mounted() {
console.log('Notifications List mounted.')
}
}
</script>
If anyone can help me that would be greatly appreciated.
You need to pass index as paramter in remove function, like following:
<notifications-item
v-for="(notification, index) in notifications"
:notification="notification"
#deleted="remove(index)"
:index="index">
{{ notification.data['text'] }}
</notifications-item>
I found a fix by adding a key attribute on the child component with a unique value (the notification id). And that's it.