So far, you've built a sweet interface to view one card and a list of all the major cards. Now, you want to show a layout to a user so that three cards can be displayed; then, you can do a 'Past/Present/Future' style reading. It's the most basic Tarot reading.

This layout is a little more complicated than the ones we've done previously; you will make a layer of three cards with titles that change color when they are selected. You'll notice that as you tap around in this screen, you will find the cards reshuffled. Can you figure out why? If you didn't want this behavior, how would you change it?

In this chapter, you'll be recreating several cards and again using the Tarot dataset, so given DRY principles ("Don't Repeat Yourself!"), it's time for a refactor. There are several ways to modularize your code to avoid repetition.

  • One would be to create reusable template files.
  • A second would be using Vuex (Vue's state management library) to share data.
  • Another is leveraging mixins to share logic throughout the app.

We will be doing this third refactor by creating a separate file for our data call and a mixin to use in several places in the app.

Prep work

First, create a reusable JavaScript file called tarot.js in the components folder. In that file, paste the methods we've used before to parse through the Tarot dataset.

import { Cards } from '../data/cards';
export default {
	name: 'Card',
	data() {
		return {
			major: true,
			name: '',
			meaning: '',
			emoji: '',
			emoji1: '',
			emoji2: '',
			icon: 'emoji',
			cards: Cards,
		};
	},
	methods: {
		getOneCard(context, sNum, rNum) {
			this.currentTab = context;
			const card = this.cards[rNum];
			this.name = card.name;
			if (card.type != 'major') {
				this.major = false;
				this.emoji1 = '~/assets/emoji/' + card.value + '.png';
				this.emoji2 = '~/assets/emoji/' + card.suit + '.png';
			} else {
				this.major = true;
				this.emoji = '~/assets/emoji/' + card.value + '.png';
			}
			if (sNum == 0) {
				this.meaning = card.meaning_up;
				this.icon = 'emoji';
			} else {
				this.meaning = card.meaning_rev;
				this.reversed = true;
				this.icon = 'emoji reversed';
			}
		},
	},
};

While you're at it, add some scoped styles to Reading.vue:

<style scoped>
    .label {
        font-family: Nunito, Nunito-Sans;
        font-size: 15;
        opacity: .5;
        text-align: center;
        margin: 15;
    }

    .selected {
        color: #5326BF;
        opacity: 1;
    }
</style>

Then, working in Reading.vue, import the tarot.js file that you just created, right under the opening <script> tag:

import Tarot from './tarot';

export default {
	mixins: [Tarot],
};

Mixins are interesting. They allow you to reuse JavaScript throughout your app, and merge in a cool way with your SFC's code. Read more here.

Build the tabbed UI

Next, create an internal tabbed interface that will layer Past, Present, and Future cards. Add a data method and a computed property under the last comma after [Tarot],

data() {
    return {
        currentTab: "Present"
    };
},
computed: {
    tabButtonClasses() {
        return tab => ({
            label: true,
            selected: tab === this.currentTab
        });
    }
},

This snippet will allow the color of a tab's label to change, similar to what we did on the first screen.

Now, you can build up the UI in <template>. Overwrite everything in the template block:

<StackLayout>
	<Label row="0" text="My Reading" class="title med" />
	<GridLayout rows="auto,1,*" columns="*" class="card">
		<GridLayout row="0" rows="*" columns="*,*,*">
			<Label col="0" :class="tabButtonClasses('Past')" text="PAST" @tap="getCard('Past')" />
			<Label col="1" :class="tabButtonClasses('Present')" text="PRESENT" @tap="getCard('Present')" />
			<Label col="2" :class="tabButtonClasses('Future')" text="FUTURE" @tap="getCard('Future')" />
		</GridLayout>
		<StackLayout row="1" backgroundColor="#8089A8" style="opacity: .2"></StackLayout>
	</GridLayout>
</StackLayout>

Notice an extra StackLayout with a low opacity - that's a sneaky way of creating a narrow Horizontal Rule type line.

Show the card stack

Now you need to add a method so that when a user taps a label, a new card appears. Add this method to a new methods property under the computed property as well as a created lifecycle hook to call it. This method will take a card's context and pass through two random numbers to select the card and its up or reversed meaning:

methods: {
    getCard(context) {
        let sNum = Math.floor(Math.random() * 2);
        let rNum = Math.round(Math.random() * 72);
        this.getOneCard(context, sNum, rNum);
    }
},
created() {
    this.getCard("Present");
}

Now, there's just one more addition to show the cards. Add the following GridLayout under the StackLayout that doubles as the Horizontal Rule:

<GridLayout row="2" rows="2*,3*,3*" class="card">
	<Label row="0" textWrap="true" class="card-title" :text="name" />
	<Image row="1" v-if="major" :class="icon" :src="emoji" />
	<StackLayout row="1" rows="*" columns="*,*" v-if="!major" horizontalAlignment="center" orientation="horizontal">
		<Image style="margin:5" v-if="!major" :class="icon" :src="emoji1" />
		<Image style="margin:5" v-if="!major" :class="icon" :src="emoji2" />
	</StackLayout>
	<Label row="2" class="meaning" textWrap="true" :text="meaning" />
</GridLayout>

Now, you should be able to tab through three cards in sequence. What's in store for you in the Past, in your Present, and in the Future? (and if you don't like what you see, keep tabbing...)

Optional: Refactor the CardOfTheDay

Now that you know how to use mixins to share logic, you could refactor your CardOfTheDay.vue to use the separate JavaScript file you created here. Deleting code feels awesome!

🎈 Yay! You have created a beautiful, functional offline-friendly mobile app to tell your Tarot! I'm sure you will do great things with this. What's in your future?