I'm having an issue regarding state management. I can achieve getting both filling my state and projecting my state onto the frontend. Except I'm having an issue with WHEN the state is getting filled. Whenever I compile my project and I go to the page in question, title: 'test' is shown instead of my api. But when I press f5 the state gets filled with my api call. Is there any reason why my state isn't being filled by the api straight away? I'm using promise, shouldn't rendering wait 'till my api request is "done"?
media/index.vue
<template>
<div>
<section class="hero is-info">
<div class="hero-body">
<div class="container">
<h1 class="title">Wordpress</h1>
<h2 class="subtitle">Media</h2>
</div>
</div>
</section>
<section>
<div class="container p-3">
<p class="title">Media</p>
<div class="posts-wrapper">
<template v-for="media in MediaList">
<nuxt-link class="card" :key="media.id" :to="'/wordpress/media/'+media.id">{{media.title.rendered}}</nuxt-link>
</template>
</div>
</div>
</section>
</div>
</template>
<script>
export default {
head () {
return {
title: 'Wordpress media'
}
},
fetch ({store}) {
store.dispatch('getMediaList')
},
computed: {
MediaList () {
return this.$store.state.getMediaList.mediaList
}
}
}
</script>
<style lang="scss">
.posts-wrapper {
display: flex;
flex-flow: row wrap;
justify-content: space-around;
a {
display: block;
margin: 1em;
padding: 1em;
flex: 1 1 200px;
text-transform: capitalize;
&:hover {
text-decoration: underline;
}
}
}
</style>
store/index.js
import Vuex from 'vuex'
import { getPosts } from './modules/wordpress/posts'
import { getPages } from './modules/wordpress/pages'
import { getMediaList } from './modules/wordpress/media'
const createStore = () => {
return new Vuex.Store({
modules: {
getPosts,
getPages,
getMediaList
}
})
}
export default createStore
store/media.js:
javascript
import API from '../../../api/request'
export const getMediaList = {
state: {
media: {},
mediaList: [{
title: {
rendered: 'test'
}
}]
},
mutations: {
setMediaList: (state, mediaList) => {
state.mediaList = mediaList
},
setCurrentMedia: (state, media) => {
state.media = media
}
},
actions: {
getMediaList ({commit}) {
return API.get(`/wp/media`)
.then((res) => {
commit('setMediaList', res.data)
})
.catch((err) => {
console.log(err)
})
},
getMedia ({commit, store}, id) {
return API.get(`/wp/media/${id}`)
.then((res) => {
commit('setCurrentMedia', res.data)
})
}
}
}
Dumb me, this did the trick:
Fetch:
async fetch ({store}) {
await store.dispatch('getMediaList')
}
Actions:
async getMediaList ({commit}) {
try {
const { data } = await API.get(`/wp/media`)
commit('setMediaList', data)
} catch (error) {
console.log(error)
}
}
Related
I use the composition api with script setup for my project and I was needing this part of the vue-select npm package. But I am not entirely sure how to convert it.
<template>
<v-select
:options="paginated"
:filterable="false"
#open="onOpen"
#close="onClose"
#search="(query) => (search = query)"
>
<template #list-footer>
<li v-show="hasNextPage" ref="load" class="loader">
Loading more options...
</li>
</template>
</v-select>
</template>
<script>
import countries from '../data/countries'
export default {
name: 'InfiniteScroll',
data: () => ({
observer: null,
limit: 10,
search: '',
}),
computed: {
filtered() {
return countries.filter((country) => country.includes(this.search))
},
paginated() {
return this.filtered.slice(0, this.limit)
},
hasNextPage() {
return this.paginated.length < this.filtered.length
},
},
mounted() {
/**
* You could do this directly in data(), but since these docs
* are server side rendered, IntersectionObserver doesn't exist
* in that environment, so we need to do it in mounted() instead.
*/
this.observer = new IntersectionObserver(this.infiniteScroll)
},
methods: {
async onOpen() {
if (this.hasNextPage) {
await this.$nextTick()
this.observer.observe(this.$refs.load)
}
},
onClose() {
this.observer.disconnect()
},
async infiniteScroll([{ isIntersecting, target }]) {
if (isIntersecting) {
const ul = target.offsetParent
const scrollTop = target.offsetParent.scrollTop
this.limit += 10
await this.$nextTick()
ul.scrollTop = scrollTop
}
},
},
}
</script>
<style scoped>
.loader {
text-align: center;
color: #bbbbbb;
}
</style>
It is not easy to reqrite that code without knowing what you tried and what external variables/scripts are available.
Maybe this gives you an idea how this could look in setup script:
<template>
<v-select
:options="paginated"
:filterable="false"
#open="onOpen"
#close="onClose"
#search="(query) => (search = query)"
>
<template #list-footer>
<li v-show="hasNextPage" ref="load" class="loader">Loading more
options...</li>
</template>
</v-select>
</template>
<script setup>
import countries from "../data/countries";
import { onMounted, computed, ref } from "vue";
const observer = ref(null);
const limit = ref(10);
const search = "";
const filtered = computed(() => {
return countries.filter((country) => country.includes(this.search));
});
const paginated = computed(() => {
return filtered.value.slice(0, this.limit);
});
const hasNextPage = computed(() => {
return paginated.value.length < this.filtered.length;
});
onMounted(() => {
observer.value = new IntersectionObserver(infiniteScroll);
});
async function onOpen() {
if (hasNextPage) {
await $nextTick();
observer.value.observe($refs.load);
}
}
function onClose() {
observer.value.disconnect();
}
async function infiniteScroll([{ isIntersecting, target }]) {
if (isIntersecting) {
const ul = target.offsetParent;
const scrollTop = target.offsetParent.scrollTop;
limit.value += 10;
await $nextTick();
ul.scrollTop = scrollTop;
}
}
</script>
<style scoped>
.loader {
text-align: center;
color: #bbbbbb;
}
</style>
I am using vue-router to write a dynamic router .and when I jump from one path to another, I find the path is indeed changed but the web page does not reload and I have no idea why.
the router definition is as follows:
const routes: Array<RouteRecordRaw> = [
{
path: '/lecture/:lecture_name',
name: 'Lecture',
component: LecturesBase,
}
]
and the dynamic web page vue components is like this:
LectureBase.vue
<template>
<div>
<a-layout>
<LectureNavigation />
<LectureTemplate
:page_name="page_name"
/>
<!-- <router-view>
</router-view> -->
<tui-juhe />
</a-layout>
</div>
</template>
<script>
import { ref } from 'vue';
import router from "#/router/index";
import { onBeforeRouteUpdate, useRoute } from "vue-router";
import LectureNavigation from "#/views/lecture/LectureNavigation.vue";
import LectureTemplate from "#/views/lecture/LectureTemplate.vue";
import TuiJuhe from "#/components/advertisement/TuiJuhe.vue";
export default {
name: "LectureBase",
setup() {
const route = useRoute();
},
components: {
LectureNavigation,
TuiJuhe,
LectureTemplate,
},
// props: {
// page_name: String, // 中文标题
// },
data() {
return {
$router: router,
page_name: ref(this.$route.params.lecture_name),
}
},
watch: {
'$route' (to, from) {
console.log('# to url: ' + to.path);
console.log('# to url: ' + this.$route.params.lecture_name);
this.page_name = this.$route.params.lecture_name;
console.log('# to url: ' + this.page_name);
this.$forceUpdate();
}
},
// methods: {
// refresh() {
// d
// }
// },
created() {
console.log("# url: " + this.$route.params.lecture_name);
}
};
</script>
<style>
</style>
LectureTemplate.vue
<template>
<a-layout class="variable_content" style="padding: 0 24px 24px">
<a-breadcrumb style="margin: 16px 0" :routes="router">
<a-breadcrumb-item>
<router-link to="/">
<home-outlined />
</router-link>
</a-breadcrumb-item>
<a-breadcrumb-item>
<router-link to="/lecture/lecture_home_page">
NLP 教程
</router-link>
</a-breadcrumb-item>
<!--a-breadcrumb-item> Home </a-breadcrumb-item-->
<a-breadcrumb-item>{{ this.title }}</a-breadcrumb-item>
</a-breadcrumb>
<a-layout-content :style="{
background: '#fff',
padding: '24px',
margin: 0,
width: '100%',
minHeight: '280px',
}">
<div>
<h1><b class="b_green">{{ this.title }}</b></h1>
<p class="description_text">
发布日期:{{ this.established_time }} 阅读量:{{ this.frontend_page_count }}
</p>
<div v-html="markdownToHtml"></div>
</div>
</a-layout-content>
</a-layout>
</template>
<script>
import { useMeta } from 'vue-meta';
import router from "../../router/index";
import { stat_instance } from "#/utils/request";
import blog_asset from "#/utils/blog_request";
import {
HomeOutlined,
} from "#ant-design/icons-vue";
export default {
name: 'LectureTemplate',
components: {
HomeOutlined,
},
// setup() {
// useMeta({ title: this.title})
// },
props: {
page_name: String, // 英文名,用于请求后端,展示 url
},
data() {
return {
router: router,
title: '',
established_time: '',
frontend_page_count: 0,
markdown: "### loading ...",
}
},
computed: {
markdownToHtml() {
var markdown_content = this.md(this.markdown);
return markdown_content;
}
},
created() {
console.log("## temp url: " + this.page_name);
stat_instance({
url: "/stat_api/frontend_page_statistics",
data: {
page_name: this.page_name,
}
})
.then((response) => {
this.frontend_page_count = response.data.frontend_page_count;
this.title = response.data.title;
this.established_time = response.data.established_time;
})
.catch(() => {
this.frontend_page_count = 0;
});
blog_asset({
url: "/lecture/" + this.page_name + "/README.md",
})
.then((response) => {
this.markdown = response.data;
})
.catch(() => {
this.markdown = "### Failed to request markdown file.";
});
}
}
</script>
<style lang="less" scoped>
h1 {
width: 100%;
// height: 80px;
font-size: 28px;
padding-top: 5px;
margin: 10px;
}
.b_green {
color: #00B441;
}
.description_text {
text-align: right;
font-size: 10px;
color: #777777;
}
</style>
So, what is wrong with my code, and the complete project code is at JioNLP_online.
You could git clone this repository and execute npm run serve to try this code and check the bug.
The program indeed captured the dynamic path of the vue-router but the web page does not changed at all.
Solution I found
I found this post about the same issue here.
Basically you need to add the :key attribute to the <vue-router> component where your page is rendered.
Example:
// this re-renders the page when the path changes
<router-view :key="$route.fullPath"></router-view>
My solution
The :key solution didn't work for me, because I didn't want the router to re-render. This would cause my smooth transition animation between the routes to break. So I tried solving it in a different way.
<script lang="ts" setup>
import { onMounted, watchEffect, ref } from "vue";
import { useRoute } from "vue-router";
const isLoading = ref<boolean>(true);
const route = useRoute();
const loadData = async (id) => {
isLoading.value = true;
// load dynamic data here and change ref values
isLoading.value = false; // after load
};
watchEffect(() => {
loadData(route.params.id);
});
onMounted(() => {
loadData(route.params.id);
});
</script>
I was practicing and learning about vuejs, specifically pagination.
I learn how to do it, and implemented it when I made the api calls in the component, then I moves the function to the actions in vuex, and mapped, them, now my pagination is not even showing up.
I wonder where I made the mistake ???
I have carefully paid attention to every single detail, and read about mutatioons, and actions, I do feel like I am doing everything right, I don't even have any mistakes showing up on the console. this is very wierd
my postModule
import axios from "axios";
export const postModule = {
state: () => ({
posts: [],
page: 1,
limit: 10,
totalPages: 0,
}),
mutations: {
setPosts(state, posts) {
state.posts = posts;
},
setPage(state, page) {
state.page = page;
},
setTotalPage(state, totalPages) {
state.setTotalPage = totalPages
},
},
actions: {
async fetchPosts( {state, commit}) {
try {
const response = await axios.get("https://jsonplaceholder.typicode.com/posts", {
params: {
_page: state.page,
_limit: state.limit,
},
});
commit('setTotalPage', Math.ceil(response.headers["x-total-count"] / state.limit));
commit('setPosts', [...state.posts, ...response.data]);
} catch (e) {
console.log(e);
}
},
async loadPosts({
state,
commit
}) {
try {
commit('setPage', state.page + 1)
setTimeout(async () => {
const response = await axios.get(
"https://jsonplaceholder.typicode.com/posts", {
params: {
_page: state.page,
_limit: state.limit,
},
});
commit('setTotalPage', Math.ceil(response.headers["x-total-count"] / state.limit));
commit('setPosts', [...state.posts, ...response.data]);
});
} catch (e) {
console.log(e);
}
},
},
namespaced: true,
}
<<<<<component >>>>
<template>
<div>
<h1>Page with the posts</h1>
<post-list :posts="posts" />
</div>
<div class="page__wrapper">
<div
v-for="pageNumber in totalPages"
:key="pageNumber"
class="page"
:class="{
'current-page': page === pageNumber,
}"
#click="changePage(pageNumber)"
>
{{ pageNumber }}
</div>
</div>
</template>
<script>
import PostList from "#/components/PostList";
// import axios from 'axios';
import { mapState, mapMutations, mapActions } from "vuex";
export default {
components: { PostList },
data() {
return {
// posts: [],
// page: 1,
// limit: 10,
// totalPages: 0,
};
},
methods: {
changePage(pageNumber) {
this.page = pageNumber;
this.fetchPosts();
},
...mapMutations({
setPage: "setPage",
}),
...mapActions({
// loadPosts: "post/loadPosts",
fetchPosts: "post/fetchPosts",
}),
},
watch: {
page() {
this.fetchPosts();
},
},
mounted() {
this.fetchPosts();
},
computed: {
...mapState({
posts: (state) => state.post.posts,
page: (state) => state.post.page,
limit: (state) => state.post.limit,
totalPages: (state) => state.post.totalPages,
}),
},
};
</script>
<style scoped>
.page__wrapper {
display: flex;
margin: 15px;
}
.page {
border: 1px solid black;
padding: 10px;
}
.current-page {
border: 2px solid red;
font-size: x-large;
}
</style>
I have a problem with vue router as defined on the title above.
Let's say I have a router-view which is renders pages dynamically when the user selected a page from the page selector component. What I'm expecting is I have to get the url to be like this:
http://localhost:port/editor/{appSlug}/layout-editor/page/{pageSlug}
But instead, I got this:
http://localhost:port/editor/{appSlug}/layout-editor/page/{pageSlug}-randomString
And the console shows this error:
NavigationDuplicated {_name: "NavigationDuplicated", name: "NavigationDuplicated", message: "Navigating to current location ("/editor/penerimaa…/page/input-pendaftaran-edrpekvl") is not allowed", stack: "Error↵ at new NavigationDuplicated (webpack-int…/node_modules/vue/dist/vue.runtime.esm.js:3876:9)"}`
I already checked the router file and still can't find out what is wrong with my route. I also tried the solution from this question but still having this error.
Can somebody please help me with this?
Please take a look at my code:
router.js
import Vue from 'vue'
import Router from 'vue-router'
import store from './store/index'
import Home from './views/home/Index.vue'
Vue.use(Router)
let router = new Router({
mode: 'history',
linkActiveClass: 'active',
linkExactActiveClass: 'exact-active',
routes: [{
path: '/',
name: 'home',
component: Home,
meta: {
requiresAuth: true
}
},
{
path: '/login',
name: 'login',
// route level code-splitting
// this generates a separate chunk (login.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('./views/auth/Login.vue'),
meta: {
requiresGuest: true
}
},
{
path: '/register',
name: 'register',
component: () => import('./views/auth/Register.vue'),
meta: {
requiresGuest: true
}
},
{
path: '/forgot-password',
name: 'forgot-password',
component: () => import('./views/auth/extras/ForgotPassword.vue'),
meta: {
requiresGuest: true
}
},
{
path: '/database',
name: 'database',
component: () => import('./views/database/Index.vue'),
meta: {
requiresAuth: true
}
},
{
path: '/third-parties',
name: 'third-parties',
component: () => import('./views/third-parties/Index.vue'),
meta: {
requiresAuth: true
}
},
{
path: '/editor',
component: () => import('./components/ViewRenderer.vue'),
meta: {
requiresAuth: true,
requiresAdmin: true,
requiresEditor: true,
},
children: [{
props: true,
path: ':appSlug/layout-editor',
name: 'layout-editor',
component: () => import('./views/editor/Index.vue'),
children: [{
props: true,
path: 'page/:pageSlug',
name: 'layout-renderer',
component: () => import('./components/LayoutRenderer.vue'), // this is where the error occured.
}],
}]
},
]
})
// Route Middlewares
router.beforeEach((to, from, next) => {
const isLoggedIn = store.getters['auth/isLoggedIn']
// Role getters
const isAdmin = store.getters['auth/isAdmin']
const isEditor = store.getters['auth/isEditor']
// Redirect to the login page if the user is not logged in
// and the route meta record is requires auth
if (to.matched.some(record => record.meta.requiresAuth) && !isLoggedIn) {
next('/login')
}
// Redirect to the homepage page if the user is logged in
// and the route meta record is requires guest
if (to.matched.some(record => record.meta.requiresGuest) && isLoggedIn) {
next('/')
}
// Redirect to the preview page if the user is logged in
// but has no role assigned or the role is user
if (to.matched.some(
record => (
record.meta.requiresAuth &&
record.meta.requiresAdmin &&
record.meta.requiresEditor
)) && isLoggedIn && isAdmin !== true && isEditor !== true) {
next('/')
}
// Pass any access if not matches any of conditions above
next()
})
export default router
Editor/Index.vue
<template>
<div class="layout-editor container-fluid">
<ActivityBar></ActivityBar>
<Sidebar title="Layout Editor">
<PalleteControl></PalleteControl>
<Pallete :items="components" :list-style="pallete"></Pallete>
</Sidebar>
<Navbar class="editor-navbar">
<PageSelector></PageSelector>
<BaseButton id="create-page-button" text="Create new page"></BaseButton>
</Navbar>
<!-- Every selected page layout rendered here -->
<ViewRenderer></ViewRenderer>
<CommitBar></CommitBar>
</div>
</template>
<script>
import components from "#/data/components.json";
import data from "#/data/table.json";
import { mapGetters } from "vuex";
export default {
name: "LayoutEditor",
data() {
return {
components,
pallete: "grid"
};
},
computed: {
...mapGetters({
current: "apps/current" // Get current app
})
},
mounted() {
this.listenPalleteEvent();
this.listenPageSelectorEvent();
},
methods: {
listenPalleteEvent() {
EventBus.$on("switch-list-style", () => this.switchPallete());
},
switchPallete() {
if (this.pallete == "grid") return (this.pallete = "list");
return (this.pallete = "grid");
},
listenPageSelectorEvent() {
EventBus.$on("page-selected", component => {
this.$router.replace({
name: "layout-renderer",
params: { pageSlug: component.pageSlug, component }
});
});
}
}
};
</script>
<style lang="scss" scoped>
.layout-editor {
padding-left: 530px;
}
</style>
components/PageSelector.vue
<template>
<BaseDropdown
id="pages-dropdown-button"
id-obj="pageId"
name-obj="pageName"
:items="filtered"
:has-item-control="true"
text="Create new page or choose one from here"
event-keyword="page-selected"
>
<BaseInput
name="page-filter"
v-model="filter"
:borderless="true"
placeholder="Search by page name..."
></BaseInput>
<template #item-control>
<div class="item-control">
<BaseButton id="duplicate-page-button" text="Duplicate"></BaseButton>
<BaseButton id="delete-page-button" text="Delete"></BaseButton>
</div>
</template>
</BaseDropdown>
</template>
<script>
import { mapGetters } from "vuex";
export default {
data() {
return {
filter: ""
};
},
created() {
// Dispatch fetch page request on vuex store when the instance was created.
this.$store.dispatch("pages/load", this.currentApp);
},
computed: {
// Map getters from vuex store.
...mapGetters({
pages: "pages/pages",
currentApp: "apps/current"
}),
// Filter pages as long as user type in the dropdown input.
filtered() {
return this.pages.filter(page => {
return page.pageName.toLowerCase().includes(this.filter.toLowerCase());
});
}
}
};
</script>
<style lang="scss" scoped>
#import "../../sass/variables";
::v-deep .dropdown-item {
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
&:hover {
.item-control {
opacity: 1;
}
}
}
::v-deep .item-control {
display: flex;
align-items: center;
justify-content: flex-end;
opacity: 0;
.form-group {
margin-bottom: 0;
}
.form-group .btn {
border-radius: 30px;
height: auto;
}
.form-group:first-child .btn {
margin-right: 5px;
}
.form-group:last-child .btn {
background-color: $red;
border-color: $red;
color: white;
&:hover {
background-color: darken($color: $red, $amount: 3);
}
}
}
</style>
components/ViewRenderer.vue
<template>
<router-view />
</template>
components/LayoutRenderer.vue
<template>
<div class="layout-renderer">
<GridLayout
:layout.sync="components"
:col-num="12"
:row-height="30"
:is-draggable="true"
:is-resizable="true"
:is-mirrored="false"
:vertical-compact="true"
:margin="[10, 10]"
:use-css-transforms="false"
:responsive="true"
:auto-size="true"
>
<GridItem
v-for="component in components"
:key="component.i"
:x="component.x"
:y="component.y"
:w="component.w"
:h="component.h"
:i="component.i"
>
<ComponentRenderer :component="component" />
</GridItem>
</GridLayout>
</div>
</template>
<script>
import { mapState } from "vuex";
import VueGridLayout from "vue-grid-layout";
export default {
components: {
GridLayout: VueGridLayout.GridLayout,
GridItem: VueGridLayout.GridItem
},
data() {
return {
components: []
};
},
created() {
this.fetchComponents();
},
methods: {
/**
* Fetch component from the backend based on the pageId
* occured by the vue-router's route parameters.
*
* #return void
*/
fetchComponents() {
let pageId = this.$route.params.component.pageId;
this.$store.dispatch("components/fetchComponents", pageId).then(() => {
this.components = this.$store.getters["components/components"];
});
}
}
};
</script>
<style lang="scss" scoped>
.layout-renderer {
margin-bottom: 100px;
}
#media only screen and (max-width: 501px) {
.vue-grid-item {
height: fit-content !important;
transform: none !important;
position: relative !important;
margin-bottom: 10px;
}
}
#media (hover: none), (hover: on-demand) {
.vue-grid-item {
height: fit-content !important;
transform: none !important;
position: relative !important;
margin-bottom: 10px;
}
}
</style>
While joyBinary's answer solves the problem, it also swallows all other errors which might not be the desired behaviour.
This approach solves this issue:
const originalPush = Router.prototype.push;
Router.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => {
if (err.name !== 'NavigationDuplicated') throw err
});
}
For those using Typescript, here's Errik Sven Puudist's answer converted:
const originalPush = Router.prototype.push;
Router.prototype.push = async function (location: RawLocation) {
let route: Route;
try {
route = await originalPush.call<Router, [RawLocation], Promise<Route>>(this, location);
} catch (err) {
if (err.name !== 'NavigationDuplicated') {
throw err;
}
}
return route!;
}
Use this code in router.js file:
const originalPush = VueRouter.prototype.push;
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err);
}
This code can override catch's exceptions.
Long time user of the wisdom of StackOverflow on and off the job, but first time I'm posting a question. What a milestone!
I'm writing unit tests for a large Vue application, and one of my components uses a method that references $route in order to determine if a query param is being passed in / pass in the param if it is being used. The method calling this.$route.query.article_id works great, however now that I am in testing, the tests don't recognize this.$route.query
I've tried to mock the $route object when using shallowMount to mount my localVue, as described in the doc, but it doesn't work, and I continue to get the same error.
Here is my component:
<template>
<b-container fluid>
<div class="content-page-header"></div>
<b-row>
<b-col cols="3" class="outer-columns text-center" style="color:grey">
<font-awesome-icon
:icon="['fas', 'newspaper']"
class="fa-9x content-page-photo mb-3 circle-icon"
/>
<br />
<br />Get practical tips and helpful
<br />advice in clear articles written
<br />by our staff's experts.
</b-col>
<b-col cols="6" v-if="articlesExist">
<h1 class="header-text">
<b>Articles</b>
</h1>
<div v-if="!selectedArticle">
<div v-for="article in articles">
<article-card :article="article" #clicked="onClickRead" />
<br />
</div>
</div>
<div v-else>
<router-link to="articles" v-on:click.native="setSelectedArticle(null)">
<font-awesome-icon icon="chevron-circle-left" /> 
<b>Back to All Articles</b>
</router-link>
<article-header :article="selectedArticle" />
<br />
<span v-html="selectedArticle.text"></span>
</div>
</b-col>
<b-col cols="6" v-else>
<h1 class="header-text">
<b>Articles</b>
</h1>
<div class="text-center">Stay tuned for more Articles</div>
</b-col>
<b-col class="outer-columns">
<b class="text-color" style="font-size:14pt">Saved Articles</b>
<div v-for="article in userArticles">
<router-link
:to="{path:'articles', query: {article_id: article.article.id}}"
v-on:click.native="setSelectedArticle(article.article)"
>
<user-article :article="article.article" />
</router-link>
<br />
</div>
</b-col>
</b-row>
</b-container>
</template>
<script>
import ArticleCard from "./ArticleCard";
import UserArticle from "./UserArticle";
import ArticleHeader from "./ArticleHeader";
import { library } from "#fortawesome/fontawesome-svg-core";
import {
faNewspaper,
faChevronCircleLeft
} from "#fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "#fortawesome/vue-fontawesome";
library.add(faNewspaper, faChevronCircleLeft);
export default {
name: "Articles",
props: [],
components: {
ArticleCard,
ArticleHeader,
UserArticle,
library,
FontAwesomeIcon,
faNewspaper,
faChevronCircleLeft
},
mixins: [],
data() {
return {
selectedArticle: null
};
},
computed: {
articles() {
return this.$store.getters.articles.filter(article => article.text);
},
articlesExist() {
return Array.isArray(this.articles) && this.articles.length;
},
userArticles() {
return this.$store.getters.userArticles;
},
articleParam() {
return parseInt(this.$route.query.article_id);
}
},
methods: {
setSelectedArticle(article) {
this.selectedArticle = article;
},
onClickRead(article) {
this.selectedArticle = article;
}
},
mounted() {
if (this.articleParam) {
this.setSelectedArticle(
this.articles.filter(article => article.id === this.articleParam)[0]
);
}
}
};
</script>
<style lang="stylus" scoped>
.text-color {
color: #549DB0;
}
.header-text {
color: white;
margin-top: -50px;
margin-bottom: 20px;
}
.outer-columns {
background-color: #F2FBFD;
padding-top: 20px;
}
.nav-back {
color: #549DB0;
background-color: #F0FBFD;
padding: 5px;
}
</style>
And here is my test:
import { shallowMount, createLocalVue } from '#vue/test-utils'
import VueRouter from 'vue-router'
import Articles from '../../../app/javascript/components/member-dashboard/Articles.vue'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(VueRouter)
localVue.use(BootstrapVue)
describe('Articles', () => {
let store
let getters
let state = {
articles: [
{
title: "Testing Vue Components"
},
{
title: "This One shows",
text: "<p>You can see me!</p>"
},
{
title: "Another One",
text: "<p>See me too!</p>"
}
],
userArticles: [
{article: {
title: "This One shows",
text: "<p>You can see me!</p>"
}},
{article: {
title: "Another One",
text: "<p>See me too!</p>"
}}
]
}
beforeEach(() => {
getters = {
articles: () => {
return state.articles
},
userArticles: () => {
return state.userArticles
}
}
store = new Vuex.Store({ getters })
})
it('only displays article with body text', () => {
const wrapper = shallowMount(Articles, {
store,
localVue
})
expect(wrapper.vm.articles.length).to.deep.equal(2)
})
})
As I mentioned, in the shallow mount, I've tried doing this:
const wrapper = shallowMount(Articles, {
store,
localVue,
mocks: {
$route: {
query: null
}
}
})
But I continue to get this error:
TypeError: Cannot read property 'query' of undefined
at VueComponent.articleParam (webpack-internal:///1:107:35)
When I remove the line return parseInt(this.$route.query.article_id); from the articleParam method, my test passes.
How do I get around this call to this.$route.query in the component? It's not necessary to my test, but is causing my test to fail when mounting the component.
import import VueRouter from 'vue-router'; in your unite test file and create a new object of the router like const router = new VueRouter(); and use it in your test case.
I have updated code here:
import { shallowMount, createLocalVue } from '#vue/test-utils'
import VueRouter from 'vue-router'
import Articles from '../../../app/javascript/components/member-dashboard/Articles.vue'
const localVue = createLocalVue()
localVue.use(Vuex)
localVue.use(VueRouter)
localVue.use(BootstrapVue);
const router = new VueRouter();
describe('Articles', () => {
let store
let getters
let state = {
articles: [
{
title: "Testing Vue Components"
},
{
title: "This One shows",
text: "<p>You can see me!</p>"
},
{
title: "Another One",
text: "<p>See me too!</p>"
}
],
userArticles: [
{article: {
title: "This One shows",
text: "<p>You can see me!</p>"
}},
{article: {
title: "Another One",
text: "<p>See me too!</p>"
}}
]
}
beforeEach(() => {
getters = {
articles: () => {
return state.articles
},
userArticles: () => {
return state.userArticles
}
}
store = new Vuex.Store({ getters })
})
it('only displays article with body text', () => {
const wrapper = shallowMount(Articles, {
store,
router,
localVue
})
expect(wrapper.vm.articles.length).to.deep.equal(2)
})
})