WebCake

Earlier this week a conversation about theme selection in Ionic 2 applications came up. For example, if you’re using a game and want the change the colors based on where the user might be in the game. Or, in the app I’m working on at the office, where the user needs to be able to choose between themes depending on the amount of ambient light around them.

I’ve updated this post from it’s original approach, which used events instead of Rxjs observables in a service. Big thanks to Abdi Saeed for the suggestion.

Note that at the time of this writing, the current release is beta.9. Please be aware that with future releases, some syntax or method names might change.

WTFL;DR: if you’re just looking for the final code, you can find it on my GitHub.

What We’re Building

So let’s take a stab at that. At the end of the day we should have:
– A simple Ionic 2 app
– A settings page
– A way for the user to choose a theme
– Proof that the changing theme works as you navigate across the app

I won’t get into how you persist the user’s settings. That part’s entirely up to you, and should fall in line with however you’re storing data and available settings options, such as the theme options that your user can select. And, since I’m not going to go into settings storage, there will be some hard-coded options in the settings service. Again, that should be driven by your own data persistence layer, which is beyond the scope of this tutorial.

Intro and Getting Started

The first steps are pretty straightforward if you’ve put together an Ionic 2 app before. If you haven’t, I’m going to skip over some of the things listed in the Getting Started section of the Ionic 2 docs. Feel free to read through to match what they’re doing to what’s covered here.

Now then, let’s start up our app. In this case I’m going to use the left-nav approach rather than the tabs approach I’ve used in the past. Feel free to go with something different.

$ ionic start themingApp sidemenu --ts --v2

If everything went well, you should be able to install whatever platforms to which you’ll deploy, and then run it in your simulator. All of the screenshots I’ll post here will be in Android, but I’ll be running in iOS as well to make sure it works.

Pages and content

At this point, you should be able to start up your app form the command line, provided you have all of your platform components installed. Way beyond the scope of this tutorial, so if you don’t have everything set up to run in your simulators, again I’ll recommend reading the Getting Started section of the Ionic 2 docs.

Since this is less about navigation or content than it is about adding functionality, I’m only going to add a single Settings page, and then use the other pages that came pre-constructed with the app for demo purposes. Those pages will reflect the changes made to the theme.

Styling themes

There are plenty of tutorials out there about basic approaches to constructing themes, so if you don’t like my approach feel free to choose another. But my plan is to create a wrapper class within the app that will be applied at the top of the DOM, and then change color settings based on that wrapper. Generally, theming applies to visual aspects like colors, which is what I’ll be working with here. So in order to do that, I’ll need to change the colors used within my app to reflect the different top-level classes that I’ll make available to my users.

Since Ionic comes with Sass as a built-in part of the build, I’ll add theme variables and apply them to elements based on whichever theme the user has selected.

What to override

Ionic, out of the box, comes with some basic elements for theming. They even document it in their guide. And that’s all good. But the way that’s constructed, the theme is determined during compilation, not dynamically as chosen by the user.

So in order to figure out what we’re going to do, we’ll have to do some digging.

If you dive into the node_modules/ionic-angular/components/app folder, you’ll see how styles are applied to fonts and background. Conveniently, it’s all applied straight to elements, which means overriding these compiled styles will be pretty simple. Additionally, we’ll also style the toolbar and list items so that we really change the overall mood. In your theme, feel free to go as dramatic as you like.

Adding Variables and Styles

We’ll add two sets of styles to the app. I’m sure there’s a shorter way of doing this, but for the sake of the tutorial I’d like things to be more straightforward, so I’m going to keep it all long-form.

In app/theme/app.variables.scss, we’ll add some variables to the bottom of the page. These will be our theme colors, and will be reflected in the user’s experience as they change themes.

// in app/theme/app.variables.scss
// below the $colors block

// take the red pill
$red-background: #fff6f6;
$red-light: #ffa99c;
$red-dark: #74000e;
// take the blue pill
$blue-background: #f6f6ff;
$blue-light: #53e1ff;
$blue-dark: #0034a7;

With those variables set, we can now add our theme .scss files. First, let’s add app.blue.scss alongside all of our other stylesheets in app/theme:

.blue-theme {
  h1, h2, h3, h4, h5, h6,
  .toolbar-title {
    color: $blue-dark;
  }
  .toolbar-background {
    background-color: $blue-light;
  }
  ion-content {
    background-color: $blue-background;
  }
  ion-list {
    .item {
      background-color: darken($blue-background, 1.5%);
    }
  }
}

Looks to me like we’ll apply these color variables as overrides on specific elements. Gnarly. Now we can do the same with a red theme, in app.red.scss:

.red-theme {
  h1, h2, h3, h4, h5, h6,
  .toolbar-title {
    color: $red-dark;
  }
  .toolbar-background {
    background-color: $red-light;
  }
  ion-content {
    background-color: $red-background;
  }
  ion-list {
    .item {
      background-color: darken($red-background, 1.5%);
    }
  }
}

With that done, we still have two things left to do. We still have yet to import these theme files into our compilation process. We’ll do that by adding imports to the app.core.scss file. Here’s what mine looks like once I add the stuffs”

// App Shared Imports
// --------------------------------------------------
// These are the imports which make up the design of this app.
// By default each design mode includes these shared imports.
// App Shared Sass variables belong in app.variables.scss.

@import "../pages/page1/page1";

@import "../pages/page2/page2";

@import "./app.blue";

@import "./app.red";

Last, we’ll apply our theme to the application. We want this theme-determining class to be as high in the DOM as possible, so that it’s applied to all the things. Well, like any Angular 2 app, Ionic 2 runs out of a top-level component; and in Ionic 2, it’s the MyApp component defined in app/app.ts. You’ll notice that the @Component decorator lists build/app.html as the linked templateUrl property. So if we open the source file, app/app.html, we’ll see the template for our base application component. Let’s wrap it in a div and add a class='blue-theme' attribute, just to get us started:

<div class="blue-theme">
    <ion-menu [content]="content">

        <ion-toolbar>
            <ion-title>Menu</ion-title>
        </ion-toolbar>

        <ion-content>
            <ion-list>
                <button menuClose ion-item *ngFor="let p of pages" (click)="openPage(p)">
                    {{p.title}}
                </button>
            </ion-list>
        </ion-content>

    </ion-menu>

    <ion-nav [root]="rootPage" #content swipeBackEnabled="false"></ion-nav>
</div>

Now run your app in the simulator to see some awesome blue stuffs!

Blue theme

Programmatically Changing Theme

Dynamic theme setting

Obviously this would be a terrible blog post if we were going to stop at a hard-coded theme. So let’s jump back into that app.html tempalte and make things a bit more programmatic. Instead of setting the class attribute directly, let’s use some one-way binding with the [class] attribute, and a public member from the MyApp component.

<div [class]="chosenTheme">
    <ion-menu [content]="content">
        <!-- the other stuffs -->
    </ion-menu>

    <ion-nav [root]="rootPage" #content swipeBackEnabled="false"></ion-nav>
</div>

Now we’ll have to define that public member for MyApp in app/app.ts, as well as set an initial value in the constructor. So let’s do that:

@Component({
    templateUrl: 'build/app.html'
})
class MyApp {
    @ViewChild(Nav) nav: Nav;

    // so here we'll intialize the public member
    // with a type of string
    chosenTheme: string;

    rootPage: any = Page1;

    pages: Array<{title: string, component: any}>

    constructor(private platform: Platform) {
        // and then we'll set a default chosen theme
        // so that the app loads up with at least one of our themes
        this.chosenTheme = 'blue-theme';

        this.initializeApp();

        // used for an example of ngFor and navigation
        this.pages = [
            { title: 'Page uno', component: Page1 },
            { title: 'Page dos', component: Page2 }
        ];
    }

    //  other initialization stuffs
}

Now if you run the app in your simulator, it should look exactly like it did when we hard coded it. That’s not exactly an improvement, but we’re getting there.

The theme settings interface

The next step is to create a simple interface for the user to change their chosen theme, which in this case will come by way of a Settings page. Personally, I like to use the Ionic CLI-tool to generate stuff for me:

$ ionic generate page settings

While I still have the app/app.ts file open, I’ll quickly add Settings, my new @Page component, to the navigation list in the constructor. We’ll import it at the top, and then add it to the array in the constructor:

// stuff
import { Page1 } from './pages/page1/page1';
import { Page2 } from './pages/page2/page2';
// consider it imported!!
import { SettingsPage } from './pages/settings/settings';

@Component({
    templateUrl: 'build/app.html'
})
class MyApp {

    // ...a bunch of stuff...

    constructor(private platform: Platform) {
        // ...more stuff...
        this.pages = [
            { title: 'Page uno', component: Page1 },
            { title: 'Page dos', component: Page2 },
            { title: 'Settings', component: SettingsPage }
        ];
    }

    // ...the rest of the stuff...
}

Now we can open that page and start rocking out. Next step is to start setting up some data so we can plug into the template. So let’s open app/settings/settings.ts. In the class definition, we’ll set up a public member that’ll house our themes, and we’ll define it in the constructor function:

@Component({
    templateUrl: 'build/pages/settings/settings.html',
})
export class SettingsPage {
    // we know that the selection is going to be a string,
    // since it matches a class attribute
    selected: String;
    // typing the choices for the select interface, like a boss
    availableThemes: {className: string, prettyName: string}[];

    constructor() {
        // yes, this is a hard-coded selection.  Good enough for the tutorial.
        // when we get to building our settings service, we'll move it there,
        // but in your app you'll want to keep this in a database somewhere.
        this.selected = 'blue-theme';
        this.availableThemes = [
            {className: 'blue-theme', prettyName: 'Blue'},
            {className: 'red-theme', prettyName: 'Red'}
        ]
    }
}

With our component all set up to distribute data to our view, we can move on to the template. We’ll want to do a few things in the template:
– set up an interface for selecting a view – in this case a select box, a la Ionic
– loop over the availableThemes property so that our template can stay dynamic to the amount of themes available
– set an initially selected theme option that reflects the default set in app.ts
– set up a change handler using standard Ionic components and their outputs

Let’s see what that looks like:

<!-- app/settings/settings.html, in all its glory! -->
<ion-navbar *navbar>
    <button menuToggle>
        <ion-icon name="menu"></ion-icon>
    </button>
    <ion-title>Settings</ion-title>
</ion-navbar>

<ion-content padding class="settings">
    <ion-list>
        <ion-item>
            <ion-label>Theme</ion-label>
            <ion-select (ionChange)="setTheme($event)">
                <ion-option *ngFor='let theme of availableThemes'
                            [value]="theme.className"
                            [checked]="theme.className === selected">
                    {{theme.prettyName}}
                </ion-option>
            </ion-select>
        </ion-item>
    </ion-list>
</ion-content>

I don’t know about you, but it looks like this block here accomplishes all the things we need. We’ve used the availableThemes property with the new fancy ngFor syntax. We’re dynamically setting the value attribute and the displayed name of the theme, in a very Angular way. We’re even dynamically setting a checked attribute, dependent on whether or not the className property of the item in the array matches our default. There are better ways to do that – storing it in your data persistence layer, as I mentioned above – but, again, scope of the tutorial.

With that markup all in place, we’re done with settings.html. Close it up and head back to settings.ts. We added in a neat ionChange handler as setTheme, which takes the value of the change event as an argument. As of now, that doesn’t exist yet. So we’ll add it to SettingsPage as a public method:

public setTheme(e) {
    console.log(e);
}

Now if you open your app, you should be able to navigate to the Settings page, and once there when you change the color, you’ll see the value you chose in your console log.

Theme selection

Very studly. Still no theme change, so let’s wire that up next.

Dynamically setting your theme as an Observable

Here’s where we’ll get into some of the fun bits of Angular 2. We’re going to create a SettingsService class that will establish the selected theme as a private Observable using the Rx.BehaviorSubject class. This will allow us to instantiate it with a starter value, and then set and get that value as the user interacts with our application. So let’s look at how that comes together in app/pages/settings/settings.service:

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs/Rx';

@Injectable()
export class SettingsService {
    // typing our private Observable, which will store our chosen theme in-memory
    private theme: BehaviorSubject<String>;
    // as promised, I've moved the availableThemes here as well
    availableThemes: {className: string, prettyName: string}[];

    constructor() {
        // initializing the chosen theme with a default.
        // NOTE: once you've wired up your persistence layer,
        // you would pull the initial theme setting from there.
        this.theme = new BehaviorSubject('blue-theme');

        // again, hard-coding the values for possible selections,
        // but you would similarly pull this from your database as well
        this.availableThemes = [
            {className: 'blue-theme', prettyName: 'Blue'},
            {className: 'red-theme', prettyName: 'Red'}
        ];
    }

    // exposing a public method to set the private theme property,
    // using the Observable.next() method, which BehaviorSubject inherits
    setTheme(val) {
        // When you've wired in your persistence layer,
        // you would send it an updated theme value here.
        // for now we're just doing things in-memory
        this.theme.next(val);
    }

    // exposing a method to subscribe to changes in the theme,
    // using the Observable.asObservable() method, which BehaviorSubject also inherits
    getTheme() {
        return this.theme.asObservable();
    }
}

So I’ve exposed a public getter and setter, which my SettingsPage and MyApp classes will use to interact with the chosen theme.

Let’s look at how that’ll happen, by first injecting it as a dependency in MyApp. Note the three major steps in getting it wired in – importing, declaring as a provider, and then calling its public methods:

// stuff
import { SettingsPage } from './pages/settings/settings';
import { SettingsService } from './pages/settings/settings.service';

@Component({
    templateUrl: 'build/app.html',
    // adding a reference to the SettingsService as a provider,
    // which will be available here, and to all children of the
    // MyApp class - which is everything
    providers: [SettingsService]
})
class MyApp {

    // ...more stuff...

    constructor(private platform: Platform, private _settings: SettingsService) {

        // using our local, private instance of the SettingsService's public method
        // to subscribe to theme changes and set a default chosen theme
        this._settings.getTheme().subscribe(val => this.chosenTheme = val);

        // ...the navigation options stuff...
    }

    // ...the rest of the stuff...
}

One more thing to do – we’ll wire up the SettingsPage to use this class as well, instead of just interacting with some locally-set strings in memory. It’s not all that complex – our typings stay the same and we don’t have any new members to declare. We just have to import our SettingsService, and then revise how we define our class members.

import { Component } from '@angular/core';
import { SettingsService } from './settings.service';

@Component({
    templateUrl: 'build/pages/settings/settings.html',
    // NOTE that we don't list SettingsService as a provider here;
    // that's because this class is already inheriting from the MyApp
    // instance, and if we redeclare it as a provider, it'll instantiate
    // a new, encapsulated version - which we don't want to do.
})
export class SettingsPage {
    selected: String;
    availableThemes: {className: string, prettyName: string}[];

    constructor(private _settings: SettingsService) {
        // now we're setting the selected property asynchronously, based
        // on the behavior of our observable theme in SettingsService
        this._settings.getTheme().subscribe(val => this.selected = val);
        // similarly, as promised, we've moved availableThemes to SettingsService,
        // and therefore need to call that property here
        this.availableThemes = this._settings.availableThemes;
    }

    // We're finally wiring in some change communication, which will allow us to
    // interact with our form, and see some changes in the theme.
    public setTheme(e) {
        this._settings.setTheme(e);
    }
}

Nailed it.

Red theme

End of the day, you should have a fully-wired theme selection interface. There’s one key thing missing, something I mentioned at the beginning of the tutorial that I won’t be covering, which is persisting user settings in a database, or window.localStorage, or wherever. That part’s up to you.

11 responses to “User-Selected Style Themes in an Ionic 2 Application”

  1. BrightPixels says:

    Thanks very much. It reads well. The only bit I got a question around is how you get/set chose theme. My instinct is to have a shared service/provider to set/get the ‘chosenTheme’ value. Will this work as well? Is there a benefit to sub/pub over the getting/setting chosen theme value via a service/provider? Is the sub/pub a common pattern in Angular apps as it’s first time I see it being used this way.

    • Colin says:

      That’s a good point. Give me another few days and I’ll have it revised to use a service instead. As an FYI, pub/sub is just a common pattern in JS, not specific to Angular. I wouldn’t say there’s a benefit over using a service – it’s just the way that I originally thought to approach the problem. But the way you’re proposing is better.

    • Colin says:

      Updated.

  2. Mark says:

    Greetings..

    Great article! Learned a lot from it. Noticed that the theme does not carry through to modals. Could you describe the changes necessary. Ionic2 beta.11..

    Cheers..

    Mark

    • Colin says:

      Interesting. Hadn’t tried in a modal, but I’ll give it a shot. I have work to do all weekend, but I’ll try to find time to spin something up Monday or Tuesday night.

  3. Mark McGreevy says:

    Hi Colin..
    Was wondering if you had any ideas yet on how to approach the theme for modals.
    Cheers..
    Mark..

    • Colin says:

      No, I haven’t. I’ve been a terrible blogger lately. We’re in SIT on our project that launches on the 12th, and I haven’t had any time to sit down at my computer. I’ll try to get to it this week. Sorry I’ve been out of touch.

  4. Roo says:

    Great article!! I would be interested in seeing your approach to modals as well.

  5. Harish Prabhakaran says:

    I just wanted to changing styling(I have 4 styles) when ever view appears. I have handled in viewWillAppear in app.component.ts, but I was seeing delay in changing styles. Please help me out.

Leave a Reply

Your email address will not be published. Required fields are marked *