I have been trying to take our existing website and re-work it using Ionic and Vue. While our current website works well as desktop website, it provides a horrible experience in a mobile situation. I am building this without any help or anyone to bounce ideas off so I am not sure if I have missed something fundamental or have just made my life soooo hard with out even realising it. Any feedback or pointers would be most appreciated?
I wanted the site to behave slightly differently between desktop and mobile:
- in desktop: have a tab bar across the top of the screen with top level navigation. If there are submenus, these appear as a sidebar on the screen
- in mobile: use the split screen with a hamburger toggle to slide in the top level menu. If there are submenus, these appear as a toolbar along the bottom of the screen.
In my App.vue
<template>
<ion-app>
<ion-tabs v-if="isDeskTopAndNotLogin">
<ion-tab-bar slot="top" class="tabBarHeader">
<ion-tab-button v-for="page in appPages" :key="page.Pos" :tab="page.Tab" @click="() => router.push(page.URL)">
<ion-label>{{page.Desc}} </ion-label>
<ion-badge v-if="page.Count > 0"> {{page.Count}}</ion-badge>
</ion-tab-button>
</ion-tab-bar>
<ion-router-outlet></ion-router-outlet>
</ion-tabs>
<ion-split-pane v-else content-id="main-content">
<ion-menu id="ionSplitPaneDiv" content-id="main-content" type="overlay">
<ion-content>
<ion-list id="labels-list">
<ion-menu-toggle auto-hide="false" v-for="(p, i) in appPages" :key="i">
<ion-item @click="selectedIndex = i" router-direction="root" :router-link="p.URL" lines="none" detail=false class="hydrated" :class="{ selected: selectedIndex === i }">
<ion-label>{{ p.Desc }}</ion-label>
</ion-item>
</ion-menu-toggle>
</ion-list>
</ion-content>
</ion-menu>
<ion-router-outlet id="main-content"></ion-router-outlet>
</ion-split-pane>
</ion-app>
</template>
This handles my two top level scenarios, the split pane for mobile and tab bar for desktop.
The routing is while it has some hierarchy but that just matches the menu/submenu options.
import { createRouter, createWebHistory } from '@ionic/vue-router';
import { RouteRecordRaw } from 'vue-router';
import store from '@/other/dataStorage';
const privateRoute = (to: any, from: any, next: any) => {
const storeage = store();
storeage.getData<string>("token").then(r => {
if ( r ) {
next();
} else {
next('login');
}
});
};
const routes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
component: () => import('@/pages/home-page.vue'),
beforeEnter: privateRoute
},
{
path: '/home/aboutme',
component: () => import('@/pages/aboutme-page.vue'),
beforeEnter: privateRoute
},
{
path: '/home/notebook',
component: () => import('@/pages/notebook-page.vue'),
beforeEnter: privateRoute
},
{
path: '/diary',
component: () => import('@/pages/diary-page.vue'),
beforeEnter: privateRoute
},
{
path: '/diary/calendar',
component: () => import('@/pages/calendar-page.vue'),
beforeEnter: privateRoute
},
{
path: '/diary/homework',
component: () => import('@/pages/homework-page.vue'),
beforeEnter: privateRoute
},
{
path: '/diary/assignment',
component: () => import('@/pages/assignment-page.vue'),
beforeEnter: privateRoute
},
{
path: '/diary/journal',
component: () => import('@/pages/journal-page.vue'),
beforeEnter: privateRoute
},
{
path: '/timetable',
component: () => import('@/pages/timetable-page.vue'),
beforeEnter: privateRoute
},
{
path: '/message',
component: () => import('@/pages/messages-page.vue'),
beforeEnter: privateRoute
},
{
path: '/info',
component: () => import('@/pages/info-page.vue'),
beforeEnter: privateRoute
},
{
path: '/settings',
component: () => import('@/pages/settings-page.vue'),
beforeEnter: privateRoute
},
{
path: '/settings/resetpassword',
component: () => import('@/pages/resetpassword-page.vue'),
beforeEnter: privateRoute
},
{
path: '/settings/wallpaper',
component: () => import('@/pages/wallpaper-page.vue'),
beforeEnter: privateRoute
},
{
path: '/login',
component: () => import('@/pages/login-page.vue'),
},
{
path: '/:pathMatch(.*)',
redirect: '/home',
beforeEnter: privateRoute
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
Then finally as each page is loaded it has a component (base-layout) that handles the submenus as required. The home pages is:
<template>
<base-layout>
<GlassPanel class="pictureFrame">
<div class="holder">
<ion-item class="someItem ion-text-center" button="false" fill="undefined" line="none">
<ion-label class="titleTop">{{diaryName}}</ion-label>
</ion-item>
<ion-img class="picture" src="https://website.com.au/appimages/coverimages/220419.220448_E04B44CA-DF64-4596-8F43-61C74E802CDB.jpeg"></ion-img>
</div>
</GlassPanel>
</base-layout>
</template>
<script lang="ts">
import { IonItem, IonLabel, IonImg } from '@ionic/vue';
import BaseLayout from '@/components/BaseLayout.vue';
import GlassPanel from '@/components/GlassPanel.vue';
import { defineComponent } from 'vue';
export default defineComponent({
name: 'home-page',
components: {
IonItem,
IonLabel,
IonImg,
BaseLayout,
GlassPanel
},
data() {
return {
userData: {
first: 'James',
preferred: 'Jimmy',
frontCover: 'https://website.com.au/appimages/coverimages/220419.220448_E04B44CA-DF64-4596-8F43-61C74E802CDB.jpeg'
}
}
},
computed: {
diaryName() {
const userName = this.userData.preferred;
const suffix = (userName.toLowerCase().endsWith('s') ? `'` : `'s`) + " Diary";
return userName + suffix;
}
}
});
</script>
The baselayout is:
<template>
<ion-page>
<HeaderLayout2 v-if="isNotDesktop()"></HeaderLayout2>
<ion-content class="baseContent">
<div v-if="isNotDesktop()" style="display: flex; height: 100%;">
<div class="allContent"><slot /></div>
</div>
<div v-else class="withSideBar">
<SideBar v-if="submenuItems?.length > 0"
:hasLogo="showLogo"
:submenus="submenuItems">
</SideBar>
<div class="allContent"><slot /></div>
</div>
</ion-content>
<ToolBar v-show="isNotDesktop() && (submenuItems?.length > 0)" :submenus="submenuItems"></ToolBar>
</ion-page>
</template>
<script lang="ts">
import HeaderLayout2 from '@/components/HeaderLayout2.vue';
import SideBar from '@/components/SideBar.vue';
import ToolBar from '@/components/ToolBarBottom.vue';
import store from '@/other/dataStorage';
import { getPlatforms, IonPage, IonContent } from '@ionic/vue';
import { defineComponent, ref } from 'vue';
interface MenuItem {
Pos: string;
Desc: string;
URL: string;
Tab: string;
Count: number
}
interface UserInterface {
last: string;
first: string;
pref: string;
cover: string;
wall: string;
}
interface ColoursInterface {
clrBackground: string;
clrHeader: string;
clrPanels: string;
clrPromptColor: string;
clrButtonText: string;
clrHoverColor: string;
clrHoverText: string;
clrContrast: string;
clrActiveText: string;
clrTimeTable: string;
clrPopupPanel: string;
clrPopupColor: string;
}
export default defineComponent({
name: 'BaseLayout',
components: {
HeaderLayout2,
SideBar,
ToolBar,
IonPage,
IonContent
},
setup() {
const storeage = store();
const submenuItems = ref([{}]);
const backgroundImg = ref('');
storeage.getData<MenuItem[]>('site').then( r => {
submenuItems.value = r.filter(i => i.Pos.substr(-1) !== '0' &&
i.URL.startsWith(window.location.pathname) &&
i.URL !== window.location.pathname );
});
storeage.getData<UserInterface>('user').then( r => {
let wallPaper = '';
if ( r.wall ) {
wallPaper = r.wall;
} else {
wallPaper = 'Default';
}
backgroundImg.value = `url("https://website.com.au/wallpaper/${wallPaper}.png")`;
});
storeage.getData<ColoursInterface>('colours').then( i => {
const root = document.querySelector(':root') as HTMLElement | null;
root?.style.setProperty('--clrBackground',i.clrBackground);
root?.style.setProperty('--clrHeader',i.clrHeader);
root?.style.setProperty('--clrPanels',i.clrPanels);
root?.style.setProperty('--clrPromptColor',i.clrPromptColor);
root?.style.setProperty('--clrButtonText',i.clrButtonText);
root?.style.setProperty('--clrHoverColor',i.clrHoverColor);
root?.style.setProperty('--clrHoverText',i.clrHoverText);
root?.style.setProperty('--clrContrast',i.clrContrast);
root?.style.setProperty('--clrActiveText',i.clrActiveText);
root?.style.setProperty('--clrTimeTable',i.clrTimeTable);
root?.style.setProperty('--clrPopupPanel',i.clrPopupPanel);
root?.style.setProperty('--clrPopupColor',i.clrPopupColor);
});
const isNotDesktop = function () : boolean {
return !getPlatforms().includes("desktop");
}
console.log('BaseLayout(setup): before return');
return {
isNotDesktop, submenuItems, backgroundImg
}
},
computed: {
showLogo(): boolean {
return window.location.pathname === "/home" ? true : false;
},
}
});
</script>
<style>
.baseContent {
--background: var(--clrBackground) v-bind(backgroundImg) bottom / cover no-repeat
}
.withSideBar {
display: flex;
height: 100%;
}
.allContent {
padding: 20px;
flex: 1 1 auto;
width: 100%;
}
.allContent_mobile {
padding: 12px;
flex: 1 1 auto;
}
</style>
Thanks for getting to the end, again any thoughts, tutorials, pointers would be most appreciated. If this is not the right space for this type of post I will remove it.