I read this part of documentation: https://vuejs.org/guide/built-ins/suspense.html#combining-with-other-components
I have in my menu 2 links: "Home" and "Tools". If I land on my root page, that is to say the "Home" route, then I go to the "Tools" route, the "Loading..." message doesn't show. I have to wait 2 seconds, though, meaning the async component waits to be loaded to be displayed.
If I go to the "Tools" route directly, I see the message. I don't understand what I did wrong because I want the "Loading..." message to show when I load this async component, even if I come from another route.
In my App.vue I have this:
<script setup>
import Connexion from '#/components/Connexion.vue'
import { ref } from 'vue'
const drawer = ref(null);
</script>
<template>
<v-app>
<v-navigation-drawer
expand-on-hover
rail>
<Suspense>
<Connexion />
<template #fallback>
Chargement...
</template>
</Suspense>
<v-divider></v-divider>
<v-list density="compact" nav>
<v-list-item prepend-icon="mdi-home" title="Home" value="home" #click="$router.push('/')"></v-list-item>
<v-list-item prepend-icon="mdi-account-multiple" title="Tools" value="tools" #click="$router.push('/tools')"></v-list-item>
</v-list>
</v-navigation-drawer>
<v-app-bar>
<v-toolbar-title>My app</v-toolbar-title>
</v-app-bar>
<v-main fluid fill-width>
XXX
<v-container
fluid
>
<router-view v-slot="{ Component }">
<template v-if="Component">
<Transition mode="out-in">
<keep-alive>
<Suspense>
<component :is="Component" />
<template #fallback>
Loading...
</template>
</Suspense>
</keep-alive>
</Transition>
</template>
</router-view>
</v-container>
</v-main>
</v-app>
</template>
<style>
/* we will explain what these classes do next! */
.v-enter-active,
.v-leave-active {
transition: opacity 0.2s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>
And here is the Tools.vue component:
<script>
import { defineComponent } from "vue";
export default defineComponent({
async setup() {
const apiUrl = import.meta.env.VITE_APP_API_URL;
console.log("loading...")
const demandes = await fetch(`${apiUrl}/tools`)
.then((r) => r.json())
.catch((e) => {
console.log(e);
return [];
});
await new Promise(r => setTimeout(r, 2000));
console.log("LOADED!")
return {
demandes,
}
}
})
</script>
<template>
<div>
<h1>Tools</h1>
<ul>
<li
v-for="tool in tools"
:key="`tool-${tool.id}`"
>
{{ tool }}
</li>
</ul>
</div>
</template>
If it has any interest, here is my router file:
import { createRouter, createWebHistory } from 'vue-router';
import { nextTick } from 'vue';
const routes = [
{
path: '/',
name: 'Home',
component: () => import('#/views/Home.vue'),
meta: {
title: 'Home',
},
},
{
path: '/tools',
name: 'Tools',
component: () => import('#/views/Tools.vue'),
meta: {
title: 'Tools',
},
},
];
const router = createRouter({
base: import.meta.env.VITE_APP_PUBLICPATH,
history: createWebHistory(import.meta.env.VITE_APP_BASE_URL),
routes,
});
const DEFAULT_TITLE = import.meta.env.VITE_APP_TITLE;
router.afterEach((to) => {
nextTick(() => {
document.title = to.meta.title ? `${DEFAULT_TITLE} - ${to.meta.title}` : DEFAULT_TITLE;
});
});
export default router;
And the Home.vue:
<script setup>
</script>
<template>
<h1>Home</h1>
</template>
<style scoped>
</style>
Related
I have a button-group component and I want this component only accepts Button component that I created as slots.
So this is my ButtonGroup component:
<template>
<div class="button-group">
<slot />
</div>
</template>
<script lang="ts">
export default {
name: 'ButtonGroup',
components: {
Button,
},
};
</script>
How can I accept only Button component as slot?
use render function
<script>
import {h} from 'vue';
export default {
name: 'ButtonGroup',
render() {
const buttons = []
for (let defaultElement of this.$slots.default()) {
// case: <button />
if (defaultElement.type === 'button') {
buttons.push(defaultElement)
}
// other component
// if (defaultElement.type.name === 'Button') {
// buttons.push(defaultElement)
// }
}
return h('div', {class: 'button-group'}, buttons)
}
};
</script>
I referenced here https://vuejs.org/api/render-function.html
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<template>
<div class="button-group">
<slot name="button">
<Button />
</slot>
</div>
</template>
<script>
export default {
name: 'ButtonGroup',
components: {
Button,
},
};
</script>
//Use of ButtonGroup component`enter code here`
<ButtonGroup>
<template #button />
</ButtonGroup>
I have 2 components. Modal and Navbar. I'm trying to open Modal using a button in Navbar using vuex. I have a state called modalIsOpen. This states value changes from false to true when clicked on the button in Navbar but only a blank row is rendered as a modal and modal content is not shown. I could not figure out what is wrong.
At the beginning i thought it was a vuetify v-dialog problem. But ive tried other libraries too. And as i said nothing worked yet.
Here is the components ,app.vue and store.js.
AddUpdateModal.vue:
<template>
<v-dialog>
<v-card width="50%" height="50%">
<v-card-title class="text-h5 grey lighten-2">
Privacy Policy
</v-card-title>
<v-card-text>
Lorem
</v-card-text>
<v-divider></v-divider>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" text #click="dialog = false">
I accept
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
export default {
name: "AddUpdateModal",
components: {},
data: () => ({}),
}
</script>
<style>
.v-card {
display: block !important;
}
.v-easy-dialog {
height: 500px !important;
}
</style>
NavBar.vue:
<template>
<div id="Navbar">
<v-row>
<v-btn color="primary" #click.stop="openModal" rounded>
Add / Update
</v-btn>
<v-btn color="primary" #click="closeModal" rounded>
Refresh
</v-btn>
</v-row>
<v-row>
<v-divider class="grey darken-4"></v-divider>
</v-row>
</div>
</template>
<script>
export default {
name: 'NavBar',
components: {},
data: () => ({}),
methods: {
openModal() {
this.$store.commit('openModal');
},
closeModal() {
this.$store.commit('closeModal')
}
},
}
</script>
App.vue:
<template>
<v-app>
<v-container>
<NavBar />
<br>
<div class="parent">
<!-- <AddButton></AddButton> -->
<v-btn #click="konsol">Konsol</v-btn>
<div id="modal" v-if="$store.state.modalIsOpen">
<template>
<AddUpdateModal></AddUpdateModal>
</template>
</div>
<v-row>
<v-col>
<DataTable />
</v-col>
<v-divider class="grey darken-4" vertical inset></v-divider>
<v-col>
<PieChart />
</v-col>
</v-row>
</div>
</v-container>
</v-app>
</template>
<script>
import NavBar from './components/NavBar.vue';
import PieChart from './components/PieChart.vue';
import DataTable from './components/DataTable.vue';
// import AddButton from './components/AddButton';
import AddUpdateModal from './components/AddUpdateModal';
// eslint-disable-next-line no-unused-vars
import store from './store/store'
// import axios from 'axios';
export default {
name: 'App',
components: {
NavBar,
AddUpdateModal,
PieChart,
DataTable,
// AddButton,
},
created() {
this.$store.dispatch("initApp")
},
data: () => ({
}),
methods: {
konsol() {
console.log("modalIsOpen", this.$store.state.modalIsOpen)
}
},
};
</script>
<style>
* {
margin: 5px;
}
.v-dialog__container {
display: unset !important;
position: relative !important;
}
</style>
store.js:
import Vue from "vue";
import Vuex from "vuex";
import axios from "axios";
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
binance24HrData: [],
modalIsOpen: false,
},
mutations: {
initPortfolioDetails(state, newCoin) {
state.binance24HrData = newCoin;
},
openModal(state) {
state.modalIsOpen = true;
},
closeModal(state) {
state.modalIsOpen = false;
},
},
actions: {
initApp(context) {
axios
.get("https://api2.binance.com/api/v3/ticker/24hr")
.then((response) => {
context.commit("initPortfolioDetails", response.data);
console.log("Binance", response.data);
});
},
openModal({ commit }) {
commit("openModal");
},
closeModal({ commit }) {
commit("closeModal");
},
},
getters: {
getCoinsDetails(state) {
return state.binance24HrData;
},
},
});
export default store;
main.js:
import Vue from "vue";
import App from "./App.vue";
import vuetify from "./plugins/vuetify";
import store from "./store/store.js";
Vue.config.productionTip = false;
new Vue({
vuetify,
store,
render: (h) => h(App),
}).$mount("#app");
I figured it out: all I had to do was add v-model to v-dialog. I thought it was unnecessary because I already had a v-if that wrapped the component containing the v-dialog. I assumed that with this requirement fulfilled, it should render the child component, but it didn't because I didn't have v-model in v-dialog.
The connection in between components gets disrupted. Which shouldnt happen since I am using bootstrap-vue inbuilt router link (using to=" " instead of href="").
The app works perfectly fine when running without dist.
App.vue
<template>
<div class="container">
<app-header></app-header>
<div class="row">
<div class="col-xs-12">
<transition name="slide" mode="out-in">
<router-view></router-view>
</transition>
</div>
</div>
</div>
</template>
<script>
import Header from "./components/Header.vue";
export default {
components: {
appHeader: Header
},
created() {
this.$store.dispatch("initStocks");
}
};
</script>
Header.vue
<template>
<div>
<b-navbar toggleable="lg" type="dark" variant="info">
<b-navbar-brand to="/">Stock Broker</b-navbar-brand>
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-collapse id="nav-collapse" is-nav>
<b-navbar-nav>
<b-nav-item to="/portfolio">Portfolio</b-nav-item>
<b-nav-item to="/stocks">Stocks</b-nav-item>
<b-nav-item #click="endDay">End Day</b-nav-item>
<b-navbar-nav right>
<b-nav-item right>Funds: {{ funds }}</b-nav-item>
</b-navbar-nav>
</b-navbar-nav>
</b-collapse>
</b-navbar>
</div>
</template>
<script>
import { mapActions } from "vuex";
export default {
data() {
return {
isDropdownOpen: false
};
},
computed: {
funds() {
return this.$store.getters.funds;
}
},
methods: {
...mapActions({
randomizeStocks: "randomizeStocks",
fetchData: "loadData"
}),
endDay() {
this.randomizeStocks();
},
saveData() {
const data = {
funds: this.$store.getters.funds,
stockPortfolio: this.$store.getters.stockPortfolio,
stocks: this.$store.getters.stocks
};
this.$http.put("data.json", data);
},
loadData() {
this.fetchData();
}
}
};
</script>
vue.config.js
module.exports = {
pluginOptions: {
prerenderSpa: {
registry: undefined,
renderRoutes: ["/", "/portfolio", "/stocks"],
useRenderEvent: true,
headless: true,
onlyProduction: true
}
}
};
router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/stocks',
name: 'Stocks',
component: () => import(/ '../views/Stocks.vue')
},
{
path: '/portfolio',
name: 'Portfolio',
component: () => import('../views/Portfolio.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
I figured it a minute after making this post haha.
The issue was that in App.vue I had my main div with a class of "container" instead of id of "app".
Corrected below:
<template>
<div id="app"> //correction here
<app-header></app-header>
<div class="row">
<div class="col-xs-12">
<transition name="slide" mode="out-in">
<router-view></router-view>
</transition>
</div>
</div>
</div>
</template>
I am attempting to refactor my code to use vuex. I am getting 2 errors: app.js:81010 [Vue warn]: Error in mounted hook: "ReferenceError: $store is not defined" and ReferenceError: $store is not defined. I think I imported vuex properly.
My goal is to update my bootstrap-vue data-table with the employee data from my database using vuex.
In the EmployeeDataTable.vue file I have a getEmployees method in methods: {} which I would like it to dispatch the fetchAllEmployees action from employees.js. fetchAllEmployees should grab all of the employees from the database and save the result to the employees.js employees: [] state.
I am now confused and need help getting in the right direction to fix this issue.
I don't know if I needed to show all of this code, but I wanted to show the flow of my components.
Entry point App.js:
import Vue from 'vue';
import store from './store';
import router from './router';
import { BootstrapVue, IconsPlugin } from 'bootstrap-vue'
import App from './components/App';
Vue.use(BootstrapVue);
Vue.use(IconsPlugin);
require('./bootstrap');
const app = new Vue({
el: '#app',
components: {
App,
},
router,
store,
});
Vuex Store index.js:
import Vue from 'vue';
import Vuex from 'vuex';
import Employees from './modules/employees';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
Employees,
}
});
Vuex module employees.js:
const state = {
employees: [],
employeesStatus: null,
};
const getters = {
allEmployees: (state) => state.employees
};
const actions = {
fetchAllEmployees({commit, state}) {
commit('setPostsStatus', 'loading');
axios.get('/api/employees')
.then(res => {
commit('employees', res.data);
commit('employeesStatus', 'success');
})
.catch(error => {
commit('setEmployeesStatus', 'error');
});
},
};
const mutations = {
setEmployees(state, employees) {
state.employees = employees;
},
setEmployeesStatus(state, status) {
state.employeesStatus = status;
}
};
export default {
state, getters, actions, mutations,
};
App.vue:
<template>
<div>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: "App"
}
</script>
<style scoped>
</style>
DashBoard.vue:
<template>
<div>
<b-container>
<b-row>
<b-col class="col-12 col-sm-12 col-md-5 col-lg-4 col-xl-4">
<b-list-group class="d-flex horiz mx-5">
<b-list-group-item class="list-group-item-padding">
<b-link v-on:click="component='home'">
<i class="fas fa-home"></i>
<span class="custom-sm-d-none">Home</span>
</b-link>
</b-list-group-item>
<b-list-group-item class="list-group-item-padding">
<b-link v-on:click="component = 'projects'">
<i class="fas fa-project-diagram"></i>
<span class="custom-sm-d-none">Projects</span>
</b-link>
</b-list-group-item>
<b-list-group-item class="list-group-item-padding">
<b-link v-on:click="component = 'employees'">
<i class="fas fa-user"></i>
<span class="custom-sm-d-none">Employees</span>
</b-link>
</b-list-group-item>
<b-list-group-item class="list-group-item-padding">
<b-link v-on:click="component = 'customers'">
<i class="fas fa-users"></i>
<span class="custom-sm-d-none">Customers</span>
</b-link>
</b-list-group-item>
<b-list-group-item class="list-group-item-padding">
<b-link v-on:click="component = 'batch-create-material-list'">
<i class="fas fa-toolbox"></i>
<span class="custom-sm-d-none">Materials</span>
</b-link>
</b-list-group-item>
<b-list-group-item class="">
<b-link v-on:click="component = 'product-list'">
<i class="fas fa-clipboard-list icon-5x"></i>
<span class="custom-sm-d-none">Tasks</span>
</b-link>
</b-list-group-item>
</b-list-group>
</b-col>
<b-col class="col-12 col-md-7 col-lg-8 col-xl-8">
<keep-alive>
<component v-bind:is="component"></component>
</keep-alive>
</b-col>
</b-row>
</b-container>
</div>
</template>
<script>
import Home from '../../components/admin/Home';
import Projects from '../../components/admin/Projects';
import Employees from '../../components/admin/Employees';
import Customers from '../../components/admin/Customers'
import ProductList from '../../components/admin/ProductList';
import CreateProductAndCategory from '../../components/admin/CreateProductAndCategory';
export default {
name: 'Dashboard',
components: {
'home': Home,
'projects': Projects,
'employees': Employees,
'customers': Customers,
'product-list': ProductList,
'batch-create-material-list': CreateProductAndCategory,
},
data() {
return {
component: 'product-list',
}
},
}
</script>
<style scoped>
/* small screen below 768px width */
#media only screen and (max-width : 691px) {
.custom-sm-d-none{display:none;}
.horiz {
flex-direction: row;
justify-content: space-evenly;
padding-bottom: 10px;
}
.list-group-item-padding {
margin-right: 10px;
}
}
</style>
Component Employees.vue:
<template>
<div>
<EmployeeDataTable/>
<CreateEmployee />
</div>
</template>
<script>
import EmployeeDataTable from "./EmployeeDataTable"
import CreateEmployee from "./CreateEmployee"
export default {
components: {
EmployeeDataTable,
CreateEmployee,
},
}
</script>
<style scoped>
</style>
Component EmployeeDataTable.vue:
<template>
<div class="overflow-auto pb-3" style="background: white; ">
<b-card
header="Employees"
header-tag="header"
>
<b-pagination
v-model="currentPage"
:total-rows="rows"
:per-page="perPage"
aria-controls="my-table"
></b-pagination>
<p class="mt-3">Current Page: {{ currentPage }}</p>
<b-table
id="employee-table"
ref="employee-table"
:items="items"
:per-page="perPage"
:current-page="currentPage"
small
></b-table>
</b-card>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
name: "EmployeeDataTable",
data() {
return {
perPage: 3,
currentPage: 1,
items: [],
}
},
computed: {
...mapGetters(['allEmployees']),
rows() {
return this.items.length
}
},
methods: {
getEmployees() {
$store.dispatch('fetchAllEmployees').then(() => {
console.log('Dispatched getEmployees method!');
});
}
},
mounted() {
this.getEmployees();
}
}
</script>
Use this.$store instead of $store in the component. Change your API call to:
axios.get('/api/employees')
.then(res => {
commit('setEmployees', res.data);
commit('setEmployeesStatus', 'success');
})
.catch(error => {
commit('setEmployeesStatus', 'error');
});
The difference now is that you're calling the mutation names. In your success commit, you had the state names instead of the mutations.
One common convention people use in Vuex is to name mutations in all caps, and it might help in a situation like this (by making it more obvious if you used a state name instead). You'd rename them to SET_EMPLOYEES and SET_EMPLOYEES_STATUS.
I have a page header (Navbar) which has a [LOG IN] link and clicking it navigates to /login route. 'login' component shows a modal form and authenticates/login()s the user and after which route is set to Home page again. At this time the header's [LOG IN] should change to ["loggedinUser"], but it does not. Here is the code
(uses bootstrap-vue for css)
./App.vue
<template>
<component :is="layout">
<router-view :layout.sync="layout"/>
</component>
</template>
<script>
export default {
name: 'App',
data () {
return {
layout: 'div'
}
}
}
</script>
./components/BaseLayout.vue
<template>
<div>
<Header/>
<slot />
<Footer/>
</div>
</template>
<script>
import Header from './Header'
import Footer from './Footer'
export default {
name: 'BaseLayout',
props: [],
components: { Header, Footer },
methods: {}
}
</script>
The Navbar is rendered via this component.
./components/Header.vue
<template>
<b-navbar
type="dark"
variant="dark"
fixed="top"
class="text-monospace text-white"
>
<b-navbar-brand to="/">MyWebapp</b-navbar-brand>
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
<b-collapse id="nav-collapse" is-nav>
<b-navbar-nav>
<b-nav-item href="#" disabled>DISABLED</b-nav-item>
</b-navbar-nav>
<!-- Right aligned nav items -->
<b-navbar-nav class="ml-auto">
<b-nav-item to="/about">ABOUT</b-nav-item>
<header-user></header-user>
</b-navbar-nav>
</b-collapse>
</b-navbar>
</template>
<script>
import HeaderUser from '#/components/HeaderUser.vue'
export default {
components: { HeaderUser }
}
</script>
"header-user" was earlier part of Header, but I made it a separate component.
./HeaderUser.vue
<template>
<div>
<b-nav-item
v-show="!isAuthenticated"
#click="signin"
>❲LOG IN❳
</b-nav-item>
<b-nav-item-dropdown
v-show="isAuthenticated"
>
<template slot="button-content">❲{{ loggedinUser }}❳</template>
<b-dropdown-item href="#" disabled>profile</b-dropdown-item>
<b-dropdown-item #click="signout">log off</b-dropdown-item>
</b-nav-item-dropdown>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
name: 'HeaderUser',
data () {
return {
authenticated: false
}
},
methods: {
...mapActions('user', ['logout']),
signin () {
this.$router.push('/login')
},
signout () {
// this.$store.dispatch('logout')
this.logout()
this.authenticated = false
this.$router.push('/landingpage')
}
},
computed: {
...mapGetters('user', ['isAuthenticated', 'getCurrentUser']),
loggedinUser: function () {
if (this.getCurrentUser) {
return this.getCurrentUser['name'].toUpperCase()
} else {
return null
}
}
}
}
</script>
Here is the login component
./pages/Login.vue
<template>
<div>
<b-modal
size="lg"
id="loginModal"
title="Credentials"
hide-footer
centered
no-close-on-backdrop
>
<b-form inline>
<b-input
class="mb-2 mr-sm-2 mb-sm-0"
id="usernameInput"
placeholder="Lab user id"
v-model="loginForm.user"
/>
<b-input
type="password"
id="passwordInput"
placeholder="Unikix domain password"
v-model="loginForm.password"
/>
<b-button #click="authenticate">Submit</b-button>
</b-form>
</b-modal>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
name: 'Login',
data () {
return {
loginForm: {
user: '',
password: '',
rememberChecked: ''
}
}
},
methods: {
...mapActions('user', ['login']),
authenticate () {
this.login(this.loginForm)
.then(() => {
this.$emit('authenticated', true) // But who's catching?
this.$router.push('/landingpage')
})
.catch(e => {
this.$router.push('/404')
})
},
},
mounted () {
this.$bvModal.show('loginModal')
}
}
</script>
Since Header and Login are separate components (no relationship), I am unable to communicate between them. After login is successful, and state has changed, Header is not aware of it and not reacting to it. the [LOG IN] remains unchanged.
How can I solve this?
(after log in is successful, if I reload the page in browser, Header is rendered with "loggedinUser" correctly.
Your store is there to share information between components.
Getters should be reactive by default, so your getter isAuthenticated should be reflected in all components when it changes, however it depends how you're writing your getters.
If you're adding properties to objects on the fly in your state, you need to use
Vue.Set/this.$set
Try and take a look at this example, it might help you out.
https://codesandbox.io/s/vuex-store-yhyj4