What's the deal with navigation?

Navigation, which often depends on a router, is often tricky in a mobile app. Should you use a side drawer for links to various pages, or should you create a tabbed interface to make your pages more visible? And what about the actual routing of your app? For nested pages, like in a master-detail page, you might want to include a back arrow in your ActionBar.

Most Vue.js apps rely on the Vue Router. Unfortunately, it's not supported for NativeScript-Vue apps, due to the nature of navigating on the web vs. navigating on mobile. There are other strategies more appropriate for mobile development, such as the NativeScript-Vue-Navigator plugin and techniques of manual routing.

For our purposes, we are going to go a 'third route' for our routing, and that is to load a stack of screens that will be reused ad hoc.

Build the stack

Let's build out the skeleton of screens that we will load ad hoc.

In components, create four Vue component files by clicking the + sign in Explorer and choosing Add file > VUE.

add file

  • CardList.vue
  • CardOfTheDay.vue
  • Home.vue
  • Reading.vue

TIP

Remember to save your work - if you see a * next to a file, hit 'save' to save it.

Now, change the default home page of your app from HelloWorld to Home by editing app.js to look like this:

import Vue from 'nativescript-vue';

import Home from './components/Home';

// Uncommment the following to see NativeScript-Vue output logs
// Vue.config.silent = false;

new Vue({

    render: h => h('frame', [h(Home)])

}).$start();

You can delete the HelloWorld.vue component now. If you look at your app on device now, it will be blank, but don't worry, we will fix it!

Add some styles

Let's change the overall look of this app by adding some styles to app.css - these the global styles. Overwrite the entire file content:

.page {
  color: #FFF;
  background: linear-gradient(to right top, #2C163D, #142237);
}

.fa {
  font-family: FontAwesome, fontawesome-webfont;
  color: #FFF;
}

.header {
    font-size: 50;
    padding: 20;
    text-align:center;
}

.med {
    font-size: 18;
}

.lg {
    font-size: 24;
}

.lab {
    margin-left: 10;
}

.title {
  text-align: center;
  text-transform: uppercase;
  margin-top: 20;
  font-family: NunitoSans-Bold, Nunito;
  font-size: 18;
  font-weight: bold;
  color: #D3DBEA;
}

.card-title {
  text-align: center;
  text-transform: uppercase;
  color: #0F1E39;
  font-family: Passion, PassionOne, PassionOne-Regular;
  font-size: 30;
}

.card {
	border-radius: 20;
	margin: 12;
    padding: 10;
	background-color: #E3E9F8;
    color: #131636;
    height: 86%;
}

.nav-btn {
    color: white;
    margin: 20;
    font-size: 40;
}

.list-item {
    margin: 3;
    padding: 5, 12;
    color: black;
    background-color: #E3E9F8;
    font-size: 22;
    border-radius: 10;
}

.list-item-name {
    font-size: 22;
    font-family: Passion, PassionOne, PassionOne-Regular;
}

.list-item-meaning {
    font-size: 15;
    font-family: Nunito, NunitoSans, NunitoSans-Regular;
    color: #131636;
}

.modal {
    height: 70%;
}

.emoji {
	text-align: center;
	padding: 10;
	font-size: 100;
	color: #333333;
    border-color: black;
    text-shadow: 0 0 3px 0 rgb(1, 0, 0);
}

.reversed {
	transform: rotate(180deg);
}

.close {
    font-size: 25;
    color: #131636;
}

.emoji_window {
    height: 80;
    width: 80;
    margin-bottom: 20;
}

In Home.vue, get ready to make a nice looking navigation bar. First, add these styles to your <style> block by overwriting the current empty styles with this:

<style>
    .nav-btn {
        color: #9D95B4;
        margin: 20;
        font-size: 30;
        padding: 10;
    }

    .purple {
        background-color: #5326BF;
        color: white;
        font-size: 30;
        border-radius: 10;
    }

    Button {
        background-color: rgba(255, 0, 0, 0.0);
        border-color: rgba(255, 0, 0, 0.0);
        border-width: 1;
    }
</style>

Hey, there's no change to the app! Why?

Well, you need to change the markup of the Home.vue component to pick up these new styles. Replace the template block in Home.vue with this:

<template>
    <Page class="page" actionBarHidden="true">
        <GridLayout rows="auto, *, auto" columns="*, *, *">

        </GridLayout>
    </Page>
</template>

Everything got all dark and gloomy! This is good. Note the use of a css gradient, giving visual interest to the background.

Note the use of the GridLayout. This is probably the most useful and flexible layout module in NativeScript. You can control how rows and columns handle the display of their content by setting the children to * or auto or a pixel value in width. NativeScript layouts are quite different from the web html you might be used to. For a tutorial, visit nslayouts.

Add a logo that will remain static throughout the app. Download this file, ensuring that it is named logo.png and upload it to a new folder that you create in the app root, assets.

logo

Then right under the GridLayout opening tag, add the logo to the layout:

<Image src="~/assets/logo.png" width="195" marginTop="20" row="0" col="0" colSpan="3" />

TIP

Upload images to the assets folder by clicking on the folder image and selecting Upload Resources from the + menu. Refresh your app, you should see the TarotMoji logo at the top.

Create a stack of components

Now, get ready to add your components to a stack in this screen. When the user clicks on a tab button, the stack will shift to show the selected view.

In Home.vue, right under the <script> opening tag, import the three files you created above:

import CardOfTheDay from '../components/CardOfTheDay';
import CardList from '../components/CardList';
import Reading from '../components/Reading';

Add a comma after the data() method, and then add a components property so that the components are able to be referenced programmatically:

components: {
	CardOfTheDay, CardList, Reading
}

Then, you need to reference an initial view to show as well as an array of these views with which the user can interact. Replace the returned data object (return {};) with the following currentComponent string and a componentsArray:

return {
	currentComponent: 'CardOfTheDay',
	componentsArray: ['CardOfTheDay', 'CardList', 'Reading'],
};

Create the navigation button bar

We're going to simulate a tab navigator with three icons by creating a GridLayout with some styles.

Why not a real tabnav? By default, the TabNavigation on Android looks considerably different than on iOS - it's at the top, rather than at the bottom. For this app design, we need something a bit more custom.

Under the logo image, add three buttons:

<Button class="purple" @tap="currentComponent = 'CardOfTheDay'" row="2" col="0" />
<Button class="purple" @tap="currentComponent = 'CardList' " row="2" col="1" />
<Button class="purple" @tap="currentComponent = 'Reading'" row="2" col="2" />

If you look carefully at our GridLayout, the numbers don't quite add up. Note the row of these items. If the logo image is listed as row 0, and the buttons are all on row 2, we're missing row 1. That's going to be the stack of components, so let's add those now. Add a component right into the markup under the logo:

<component
	v-for="component in componentsArray"
	v-show="component === currentComponent"
	:is="component"
	row="1"
	col="0"
	colSpan="3"
/>

At this point you should see three purple buttons at the bottom of the screen and some white text in the middle.

There's some fancy stuff going on here.

  • The component on row 1 is made up of components within the componentsArray that we created earlier. They are rendered sequentially via v-for.
  • In addition, a component should only be shown if it's the currentComponent (which, now, is CardOfTheDay).
  • There's also the shorthand :is which is a special attribute of a dynamic component. Such a component can change when the current view changes, in our architecture.

To test that the stack of components shows a given view, edit the <Label text> in each of the components to reference the name of the component (for example <Label text="Reading" />). Clicking on the purple buttons at the bottom should let your views cycle.

Now that the stack of views is showing as requested, let's change the styles of the buttons programmatically as well to enhance the usability of the app.

Change the button styles

The buttons are blank and not very attractive, just stuck in a purple state. Let's fix them.

We want to use fonticons in our app to show an icon in each button. There's a plugin you can use, but it's a little easier to simply use the unicode of the fonticon.

First remove the temporary purple classes which are added to the button just so we can see them. (class="purple"). Add a text property, making sure it's text.decode:

<Button text.decode="&#xf0c8;" @tap="currentComponent = 'CardOfTheDay'" row="2" col="0" />
<Button text.decode="&#xf0c9;" @tap="currentComponent = 'CardList' " row="2" col="1" />
<Button text.decode="&#xf24d;" @tap="currentComponent = 'Reading'" row="2" col="2" />

Now, you see some unintelligible symbol in the buttons, and they're no longer purple. That's because we haven't added the fonticon fonts yet!

In the root of your app, let's add all the fonts we're going to need:

Download them here to your local machine, unzip the folder, and then in the Playground, select the app folder and click Upload folder in the small dot menu on the right. Choose the fonts directory inside the archive you just unzipped (the directory that contains the four font files). If all goes well, you will have uploaded a fonts folder with four font files.

To get the fonts to show in your app, you need to use a computed property and a dynamic class.

Computed properties are important elements of many Vue.js apps. They are functions that can calculate a value and return data that will be used in your template. In our case, our computed property will assign a css class depending on whether the current view aligns to the the button selected as well as bind css classes to the button programmatically.

First, add a computed property under the last comma of the data method:

computed: {
    navigationButtonClasses() {
        return component => ({
            "fa": true,
            "nav-btn": true,
            "purple": component === this.currentComponent
        });
    }
},

All the buttons are computed as "fa nav-btn", but depending on the selected component, one more class, "purple", will be added or removed. To make this work, add a binded property to each button's class, such that your buttons now look like this:

<Button
	:class="navigationButtonClasses('CardOfTheDay')"
	text.decode="&#xf0c8;"
	@tap="currentComponent = 'CardOfTheDay'"
	row="2"
	col="0"
/>
<Button
	:class="navigationButtonClasses('CardList')"
	text.decode="&#xf0c9;"
	@tap="currentComponent = 'CardList' "
	row="2"
	col="1"
/>
<Button
	:class="navigationButtonClasses('Reading')"
	text.decode="&#xf24d;"
	@tap="currentComponent = 'Reading'"
	row="2"
	col="2"
/>

Now, your buttons should start to look nice! The fonticon is colored gray and has no background when the selected view does not correlate to it, but when it does, the icon is white and the button is a nice purple.

💪 Progress!

Now we need to start building out the individual page screens.