As a kid, advent calendars were one of my favourite Christmas traditions. There was something special about ripping open that perforated cardboard each day and eating chocolate first thing in the morning. As an adult, I would prefer one of those whiskey advent calendars, but 24 little bottles of bourbon work out to be quite expensive for Australians.
Although my wish for whiskey may not be fulfilled until another Christmas, I wanted to try something a little festive for a tutorial. We are going to build an advent calendar in Ionic, complete with animations for revealing each day, and a restriction so that each day will remain “locked” until that day actually arrives. There will be no cheating on this calendar!
Here’s what it will look like when it is done:
Each individual day can be flipped to reveal the other side (only if the “unlock date” has passed). This actually uses the flash card component that we built a while ago in another tutorial (which was based on David Walsh’s work. Each day can be individually configured with content, and once the user flips a particular day this will be saved into storage so that it is already flipped the next time they visit the application.
Before We Get Started
Last updated for Ionic 3.9.2
Before you go through this tutorial, you should have at least a basic understanding of Ionic concepts. You must also already have Ionic set up on your machine.
If you’re not familiar with Ionic already, I’d recommend reading my Ionic Beginners Guide or watching my beginners series first to get up and running and understand the basic concepts. If you want a much more detailed guide for learning Ionic, then take a look at Building Mobile Apps with Ionic.
1. Generate a New Ionic Application
We are going to start by generating a new Ionic project, and we are also going to set up the custom component and provider that we need.
Run the following command to create a new Ionic project:
ionic start ionic-advent blank
Make the new project your working directory:
cd ionic-advent
Generate the FlashCard component:
ionic g component FlashCard
Generate the AdventDays provider:
ionic g provider AdventDays
We will use the FlashCard to represent each of our days in the advent calendar, and we will use the AdventDays provider to manage saving, loading, and modifying the data related to each day. We also need to make sure that these are added to our root module file, and since we will be using local storage to save data we will need to set up the IonicStorageModule as well.
Modify src/app/app.module.ts to reflect the following:
import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core';
import { IonicApp, IonicErrorHandler, IonicModule } from 'ionic-angular';
import { IonicStorageModule } from '@ionic/storage';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';
import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { FlashCardComponent } from '../components/flash-card/flash-card';
import { AdventDaysProvider } from '../providers/advent-days/advent-days';
@NgModule({
declarations: [MyApp, HomePage, FlashCardComponent],
imports: [
BrowserModule,
IonicStorageModule.forRoot(),
IonicModule.forRoot(MyApp),
],
bootstrap: [IonicApp],
entryComponents: [MyApp, HomePage],
providers: [
StatusBar,
SplashScreen,
{ provide: ErrorHandler, useClass: IonicErrorHandler },
AdventDaysProvider,
],
})
export class AppModule {}
With that done, we are ready to start building our advent calendar.
2. Implement the Flash Card Component
The first thing we are going to do is implement the flash card component. As I mentioned previously, we built this component in a previous tutorial. The flash card is a box that can be tapped to “flip” it, and content can be displayed on both the front and the back. By building this feature as a component, we can easily use and reuse it anywhere in the application by adding this to a template:
<flash-card>
<div class="flash-card-front">This will be displayed on the front</div>
<div class="flash-card-back">This will be displayed on the back</div>
</flash-card>
The actual code to make this component is a lot more complex than what you see above, but we can hide all of that away in the component so that we only ever have to worry about it once. We are going to keep this component mostly the same, so I won’t be talking about it in depth here. However, we will need to make a couple of changes to the way the component works.
We are going to modify the component so that the “cards” that will represent our days will have a fixed square size. We also need some slightly different behaviour. The way we designed it in the previous tutorial allows the card to be flipped when it is tapped, and all of this is handled inside of the component. We will need to modify the component so that we can control the flipping behaviour from outside of the component. We want to add some extra logic around when a card can be flipped (only when the appropriate date has passed) and we don’t want to allow cards to be “unflipped”.
To achieve this, we will add an input to the component so that we can supply the “flipped” value from data that will come from outside of the component. We will also handle the (click)
event from outside of the component. If you are unfamiliar with inputs and outputs, or even custom components in general, I would recommend watching Custom Components in Ionic.
Once we have made out modifications, we will be able to use the component like this in our template:
<flash-card [flipped]="day.flipped" (click)="flip(day)">
<div class="flash-card-front">{{ day.front }}</div>
<div class="flash-card-back">{{ day.content }}</div>
</flash-card>
It’s mostly the same, but now we can supply the flipped input using whatever data we want. Let’s implement the component now. If you want more detail about how it works, make sure to read or watch the flash card component tutorial.
Modify src/components/flash-card/flash-card.html to reflect the following:
<div class="flip-container" [class.flipped]="flipped">
<div class="flipper">
<div class="front">
<ng-content select=".flash-card-front"></ng-content>
</div>
<div class="back">
<ng-content select=".flash-card-back"></ng-content>
</div>
</div>
</div>
Modify src/components/flash-card/flash-card.ts to reflect the following:
import { Component, Input } from '@angular/core';
@Component({
selector: 'flash-card',
templateUrl: 'flash-card.html',
})
export class FlashCardComponent {
@Input('flipped') flipped: boolean = false;
constructor() {}
}
Modify src/components/flash-card/flash-card.scss to reflect the following:
.ios,
.md {
flash-card {
/*
* Flip animation by David Walsh: https://davidwalsh.name/css-flip
*/
/* entire container, keeps perspective */
.flip-container {
perspective: 1000px;
}
/* flip the pane when hovered */
.flip-container.flipped .flipper {
transform: rotateY(180deg);
}
.flip-container,
.front,
.back {
width: 100px;
height: 100px;
margin: auto;
}
/* flip speed goes here */
.flipper {
transition: 0.6s;
transform-style: preserve-3d;
position: relative;
}
/* hide back of pane during swap */
.front,
.back {
display: flex;
align-items: center;
justify-content: center;
background-color: #ecf0f1;
backface-visibility: hidden;
-webkit-box-shadow: 0px 8px 4px -4px rgba(163, 163, 163, 0.25);
-moz-box-shadow: 0px 8px 4px -4px rgba(163, 163, 163, 0.25);
box-shadow: 0px 8px 4px -4px rgba(163, 163, 163, 0.25);
border: 1px solid #dee2e3;
margin: 0;
position: absolute;
top: 0;
left: 0;
}
/* front pane, placed above back */
.front {
z-index: 2;
/* for firefox 31 */
transform: rotateY(0deg);
}
/* back, initially hidden pane */
.back {
transform: rotateY(180deg);
}
}
}
This is all we need to make the component work, we will make use of it in our application in a little bit.
3. Implement the Advent Days Provider
Next, we are going to implement the AdventDays provider. We could do this work directly in the page that is hosting the advent calendar, but wherever possible it is a good idea to separate “work” out into providers and keep the pages as simple as possible.
Modify src/providers/advent-days/advent-days.ts to reflect the following:
import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage';
interface AdventDay {
front: string,
unlockAt: string,
flipped: boolean,
content: string
}
@Injectable()
export class AdventDaysProvider {
public adventDays: AdventDay[];
constructor(private storage: Storage) {
}
loadDays(){
this.storage.get('adventDays').then((days) => {
if(days !== null){
this.adventDays = days;
} else {
this.adventDays = [
{front: '14', unlockAt: 'December 14, 2017 00:00:00', flipped: false, content: 'A partridge in a pear tree'},
{front: '15', unlockAt: 'December 15, 2017 00:00:00', flipped: false, content: 'Two turtle doves'},
{front: '16', unlockAt: 'December 16, 2017 00:00:00', flipped: false, content: 'Three French hens'},
{front: '17', unlockAt: 'December 17, 2017 00:00:00', flipped: false, content: 'Four calling birds'},
{front: '18', unlockAt: 'December 18, 2017 00:00:00', flipped: false, content: 'Five golden rings!'},
{front: '19', unlockAt: 'December 19, 2017 00:00:00', flipped: false, content: 'Six geese a laying'},
{front: '20', unlockAt: 'December 20, 2017 00:00:00', flipped: false, content: 'Seven swans a swimming'},
{front: '21', unlockAt: 'December 21, 2017 00:00:00', flipped: false, content: 'Eight maids a milking'},
{front: '22', unlockAt: 'December 22, 2017 00:00:00', flipped: false, content: 'Nine ladies dancing'},
{front: '23', unlockAt: 'December 23, 2017 00:00:00', flipped: false, content: 'Ten lords a leaping'},
{front: '24', unlockAt: 'December 24, 2017 00:00:00', flipped: false, content: 'Eleven pipers piping'},
{front: '25', unlockAt: 'December 25, 2017 00:00:00', flipped: false, content: '12 drummers drumming'}
];
}
});
}
saveDays(){
this.storage.set('adventDays', this.adventDays);
}
flipDay(day){
if(!day.flipped && new Date(day.unlockAt) < new Date()){
day.flipped = true;
this.saveDays();
}
}
}
We define an interface that describes the structure of the objects that will represent the days in our advent calendar. It is not necessary to create an interface, but it allows us to enforce that all data that we add conforms to that type:
public adventDays: AdventDay[];
This means that adventDays
must be an array that contains objects of the AdventDay
type. The object itself has four properties: front, content, unlockAt, and flipped. The front and content properties are used to define the content for the front and back of the card, the unlockAt property controls when the card will be allowed to flip, and the flipped property determines whether or not the day is currently flipped.
In our loadDays
method, we attempt to load in the array of days from storage, but if it does not exist already then we have some dummy data defined that gets loaded instead. In a real-world scenario, you would likely be loading this data in from somewhere else (your server perhaps).
IMPORTANT: This is just a fun example, so security is of no great concern here. However, if you are implementing this in a scenario where it is important that user’s can not somehow “hack” there way into revealing days that have not yet been reached, you should make sure to:
- Perform the date check on a server
- Load the content for the days from a server, and only data for the days that should be revealed
It should be quite trivial to force a card to “flip” or to fake the date (all client-side code can be easily modified). By checking the date on the server and making sure that the content for days is only supplied by the server after the date has passed, you can prevent users from cheating the system.
We also have a saveDays
method which simply saves the data back to storage, and a flipDay
method that will change a days flipped
property to true (but only if it is the correct date).
4. Implement the Calendar
All we have left to do now is implement the calendar on a page, which is going to be quite easy to do.
Modify src/pages/home/home.ts to reflect the following:
import { Component } from '@angular/core';
import { AdventDaysProvider } from '../../providers/advent-days/advent-days';
@Component({
selector: 'page-home',
templateUrl: 'home.html'
})
export class HomePage {
constructor(private adventDaysProvider: AdventDaysProvider) {
}
ionViewDidLoad(){
this.adventDaysProvider.loadDays();
}
flip(day){
this.adventDaysProvider.flipDay(day);
}
}
All we need to do here is inject the AdventDays provider, and we call the loadDays
method when the page is loaded. We also set up a flip
method so that we can call that from our template and pass through the day that the user is attempting to flip to the provider.
Modify src/pages/home/home.html to reflect the following:
<ion-content>
<ion-grid>
<ion-row>
<ion-col col-4 *ngFor="let day of adventDaysProvider.adventDays">
<flash-card [flipped]="day.flipped" (click)="flip(day)">
<div class="flash-card-front">{{ day.front }}</div>
<div class="flash-card-back">{{ day.content }}</div>
</flash-card>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>
We are using a simple grid here that loops over each of the advent days with an *ngFor
structural directive. Since we use a column size of 4, we will be able to fit three days on each row. Then we just use the flash card component as we described earlier, supplying the content from our AdventDays array.
Finally, we will just add a bit of styling to pretty things up a bit.
Modify src/pages/home/home.scss to reflect the following:
.ios,
.md {
page-home {
.scroll-content {
display: flex;
background-color: #fff;
background-image: url(https://images.unsplash.com/photo-1479722842840-c0a823bd0cd6?auto=format&fit=crop&w=1952&q=80);
background-size: cover;
}
ion-grid {
justify-content: center;
}
ion-col {
margin: 20px 0;
}
.front,
.back {
background-color: #d83313;
border: 2px dashed #fff;
color: #fff;
}
.flash-card-front {
font-weight: bold;
font-size: 1.3em;
}
.flash-card-back {
padding: 10px;
text-align: center;
}
}
}
I’m just using a background image from unsplash.com here, but in a production environment, you should store that image locally. With those changes, the application should now look like this:
Summary
Aside from a bit of fun for the holidays, there are a few practical lessons in this tutorial. We’ve made use of custom interfaces/types, a custom component that uses an input, and a provider to handle loading and saving data. The feature could also be a useful in a real-world scenario for revealing surprises to the users of your application over a period of time, not necessarily just for Christmas.