I have finally figured out how to do this. There is more than one way and let me share each way here
Approach 1 Use programmatic route navigation with the same page for everything
The idea with this approach is that there is only one page that handles both list and detail views. The router defines two separate routes that both point to the same page. Whenever the route changes, we monitor it so that we can find the name of the new route and based on the name dynamically load the list or detail component
router.js
import Vue from "vue";
import Router from "vue-router";
import Index from "~/pages/index";
Vue.use(Router);
export function createRouter() {
return new Router({
mode: "history",
routes: [
{
path: "/news/:tag?",
alias: "/",
name: "NewsList",
component: Index,
props: true
},
{
path: "/news/:tag?/:id([a-fA-F\\d]{32})/:title",
name: "NewsDetail",
component: Index,
props: true
}
]
});
}
Index.vue
<template>
<div class="news__container">
<template v-if="isMobile">
<component :is="current"></component>
</template>
<template v-else>
<div class="left">
<news-list></news-list>
</div>
<div class="right">
<news-detail></news-detail>
</div>
</template>
</div>
</template>
<script>
import NewsList from "~/components/NewsList";
import NewsDetail from "~/components/NewsDetail";
export default {
name: "root",
components: { NewsList, NewsDetail },
data: () => ({
isMobile: false,
}),
beforeDestroy() {
if (typeof window !== "undefined") {
window.removeEventListener("resize", this.onResize, { passive: true });
}
},
computed: {
current() {
return this.$route.name === "NewsList" ? NewsList : NewsDetail;
},
},
mounted() {
this.onResize();
window.addEventListener("resize", this.onResize, { passive: true });
},
watch: {
$route: {
immediate: true,
handler(newRoute) {
// Set name of the current route inside a variable
// Use this variable as a computed property to dynamically load the <component> on mobile view
this.current = newRoute.name;
},
},
},
methods: {
onResize() {
this.isMobile = window.innerWidth < 768;
},
},
};
</script>
<style lang="scss" scoped>
.news__container {
display: flex;
}
.left {
flex: 1;
}
.right {
flex: 1;
}
</style>
Approach 2 Create list as the parent, detail as the nuxt child which is hidden on mobile and a separate detail page only shown on mobile (Duplication)
Here the router.js is created such that the news list page is the parent, the news detail page is the child. On the desktop the list and the detail page are shown side by side. On the mobile the detail page is hidden and a different mobile only detail page is shown. Hence the detail page is being duplicated twice in this approach. In addition to duplication, another problem with this approach is that the mobile NewsDetail page is directly accessible on the desktop
router.js
import Vue from "vue";
import Router from "vue-router";
import Index from "~/pages/index";
import Detail from "~/pages/detail";
Vue.use(Router);
export function createRouter() {
return new Router({
mode: "history",
routes: [
{
path: "/news/:tag?",
name: "TaggedNews",
component: Index,
alias: "/",
children: [
{
path: "/news/:tag?/:id([a-fA-F\\d]{32})/:title",
name: "TaggedNewsItemDesktop",
component: Detail,
props: true
}
]
},
{
path: "/news/:tag?/:id([a-fA-F\\d]{32})/:title",
name: "TaggedNewsItemMobile",
component: Detail,
props: true
}
]
});
}
The Index.vue page uses the window resize listener to set a variable isMobile which is true for width < 768
Index.vue
<template>
<div class="news__container">
<div class="left">
<news-list :is-mobile="isMobile" />
</div>
<div v-if="!isMobile" class="right">
<nuxt-child></nuxt-child>
</div>
</div>
</template>
<script>
import NewsList from "~/components/NewsList";
export default {
name: "root",
components: { NewsList },
data: () => ({
isMobile: false,
}),
beforeDestroy() {
if (typeof window !== "undefined") {
window.removeEventListener("resize", this.onResize, { passive: true });
}
},
mounted() {
this.onResize();
window.addEventListener("resize", this.onResize, { passive: true });
},
methods: {
onResize() {
this.isMobile = window.innerWidth < 768;
},
},
};
</script>
<style lang="scss" scoped>
.news__container {
display: flex;
height: 100%;
}
.left {
flex: 1;
}
.right {
flex: 1;
}
</style>
The NewsList is always shown in this approach. The detail is shown only on desktop. The mobile version of NewsDetail page is as shown below
NewsDetail.vue
<template>
<news-detail :tag="tag" :id="id" :title="title" />
</template>
<script>
import NewsDetail from "~/components/NewsDetail";
export default {
components: { NewsDetail },
props: {
tag: {
type: String,
required: true,
default: "",
},
id: {
type: String,
required: true,
default: "",
},
title: {
type: String,
required: true,
default: "",
},
},
};
</script>
Approach 3 Create both list and detail as children
Create the router.js where the News page has both the NewsList and NewsDetail pages as children
router.js
import Vue from "vue";
import Router from "vue-router";
import Index from "~/pages/index";
import NewsList from "~/pages/NewsList";
import NewsDetail from "~/pages/NewsDetail";
Vue.use(Router);
export function createRouter() {
return new Router({
mode: "history",
routes: [
{
path: "/news/:tag?",
alias: "/",
component: Index,
children: [
{
path: "",
name: "NewsList",
component: NewsList,
props: true
},
{
path: "/news/:tag?/:id([a-fA-F\\d]{32})/:title",
name: "NewsDetail",
component: NewsDetail,
props: true
}
]
}
]
});
}
The Index.vue file adds a window resize listener which calls the onResize method everytime the screen size changes. Inside this method we set the variable isMobile to true if screen width < 768. Now whenever isMobile is true, we show the appropriate nuxt child based on route and on the desktop we show the list and detail components side by side without using nuxt child. The simple idea is that the pages NewsList and NewsDetail are shown depending on the route on mobile view whereas desktop view loads both components side by side
Index.vue
<template>
<div class="news__container">
<template v-if="isMobile">
<div class="left">
<nuxt-child></nuxt-child>
</div>
</template>
<template v-else>
<div class="left">
<app-news-list />
</div>
<div class="right">
<app-news-detail
:tag="$route.params.tag"
:id="$route.params.id"
:title="$route.params.title"
></app-news-detail>
</div>
</template>
</div>
</template>
<script>
import AppNewsList from "~/components/AppNewsList";
import AppNewsDetail from "~/components/AppNewsDetail";
export default {
name: "root",
components: { AppNewsList, AppNewsDetail },
data: () => ({
isMobile: false,
}),
beforeDestroy() {
if (typeof window !== "undefined") {
window.removeEventListener("resize", this.onResize, { passive: true });
}
},
computed: {
current() {
return this.$route.name === "Index" || this.$route.name === "AppNewsList"
? AppNewsList
: AppNewsDetail;
},
},
mounted() {
this.onResize();
window.addEventListener("resize", this.onResize, { passive: true });
},
methods: {
onResize() {
this.isMobile = window.innerWidth < 768;
},
},
};
</script>
<style lang="scss" scoped>
.news__container {
display: flex;
}
.left {
flex: 1;
}
.right {
flex: 1;
}
</style>
The page NewsList simply loads a component that shows a list of news items whereas NewsDetail simply loads a detail component
NewsList.vue
<template>
<app-news-list />
</template>
<script>
import AppNewsList from "~/components/AppNewsList";
export default {
name: "NewsList",
components: { AppNewsList },
};
</script>
NewsDetail.vue
<template>
<app-news-detail :tag="tag" :id="id" :title="title" />
</template>
<script>
import AppNewsDetail from "~/components/AppNewsDetail";
export default {
name: "NewsDetail",
components: { AppNewsDetail },
props: {
tag: { type: String, required: true, default: "" },
id: { type: String, required: true, default: "" },
title: { type: String, required: true, default: "" },
},
};
</script>