Back to Blog Home
← all posts

Dynamically swap themes with the theme-switcher!

October 22, 2021 — by Igor Randjelovic

Often times you want to let the user configure the appearance of an app based on their preferences. The theme-switcher is a lightweight and efficient plugin that lets you easily switch between different sets of (s)css files taking care of unloading all styles from each theme when switching to a different one.

npm i --save @nativescript/theme-switcher
import { Dialogs } from "@nativescript/core";
import { initThemes, switchTheme } from '@nativescript/theme-switcher';

// in main.ts
initThemes({
  default: () => import('theme-loader!./themes/default.scss'),
  red: () => import('theme-loader!./themes/red.scss'),
  green: () => import('theme-loader!./themes/green.scss'),
}, {
  /* optionally pass in options here */
});

// in the button tap handler
async function onTap() {
  const res = await Dialogs.action('Select theme', 'Cancel', [
    'default', 'red', 'green'
  ])

  if(res) {
    switchTheme(res);
  }
}

How it works - a dive through the source

The plugin itself is quite simple, and it breaks into a few different parts:

  1. the plugin code itself inside index.ts
  2. a webpack loader in theme-loader.js
  3. a (nativescript) webpack configuration in nativescript.webpack.js
  4. a typescript shim to support importing css/scss files in shims.d.ts

The plugin code

The plugin consists of a ThemeSwitcher class, an exported default instance of it, and direct exports of the available methods as shortcuts.

When switching themes, the previously selected theme's css rules are cleaned up using the removeTaggedAdditionalCSS method from core. New css rules are added using addTaggedAdditionalCSS. Finally, we refresh the styles by calling _onCssStateChange() on the root view and all open modal views.

Persistence is done using the ApplicationSettings from core, and the key in which we save the last theme is configurable.

A caveat with how css/scss is loaded in @nativescript/webpack@5+

In @nativescript/webpack@5+ css and scss is loaded and applied using the addAdditionalTaggedCSS and removeAdditionalTaggedCSS methods from core and are invoked whenever you import './something.css' automatically.

When switching themes, we need to import the css, but we don't actually want the styles to be automatically applied because there's no simple way to remove them when switching to a different theme.

To make usage simple, @nativescript/theme-switcher ships with a tiny webpack loader that replaces all of the calls to addAdditionalTaggedCSS and removeAdditionalTaggedCSS with an empty no-op function call - disabling the auto-applying of the imported css/scss.

The loader is intended to be used directly in the markup by prefixing the import paths with it:

import "theme-loader!./path/to/some.css"
import "theme-loader!./path/to/some.scss"

The above will process the some.s?css file normally, running it through all the configured loaders, and will finally run it through our theme-loader to disable the loading code.

A new way to modify the webpack config

The plugin assumes you are using @nativescript/webpack@5+ and utilizes some of the new features it brings to the table. Namely, this plugin has a nativescript.webpack.js file that registers a new loader alias for theme-loader making the integration into existing NativeScript projects easy.

const { resolve } = require('path');

/**
 * @param {typeof import("@nativescript/webpack")} webpack
 */
module.exports = (webpack) => {
	webpack.chainWebpack((config) => {
		// prettier-ignore
		config.resolveLoader.alias
            .set('theme-loader', resolve(__dirname, 'theme-loader'))
	});
};

This approach opens up a whole lot of possibilities to plugin authors. ThemeSwitcher only scratches the surface of what is possible.

Read more about the Plugin API in the docs

Making TypeScript happy

TypeScript does not know how to handle .css or .scss files, and will complain when it comes across the following code

import("theme-loader!./themes.default.css");

The above truly doesn't make any sense for TypeScript, but since NativeScript apps are built using webpack - we have rules configured to be able to handle css and scss files.

The quick and dirty fix for the issue is to slap a // @ts-ignore above the import statement, and call it a day.

ThemeSwitcher ships with a shims.d.ts file that let's typescript know the above is a valid module/import.

The shim itself has the following, and doesn't declare any of the specifics, and is only used to let TypeScript know about these modules.

declare module "theme-loader!*" {}

To let TypeScript know about the shims, include them in the references.d.ts file:

/// <reference path="./node_modules/@nativescript/theme-switcher/shims.d.ts" />

That's all, if you run into issues let us know.