Theming

Overview

This tutorial will extend on previous tutorials where we created the basic biz-e-corp application. In this tutorial, we will adapt the app to allow it to be themed, then write a theme and apply it to our application. This will be done using the built-in theming system that is included in Dojo.

Prerequisites

You can download the demo project and run npm install to get started.

The @dojo/cli command line tool should be installed globally. Refer to the Dojo local installation article for more information.

You also need to be familiar with TypeScript as Dojo uses it extensively.

Theming our widgets

Theming our widgets

In Dojo, we differentiate between two types of styles:

  • Structural styles: these are the minimum necessary for a widget to function
  • Visual styles: these are themed
    The current CSS in the example app provides the structural styles, we will now review how to create and manage themes.

In order to theme our widgets, we must ensure that they each apply the ThemedMixin and change the class name of the widget’s top-level node to root. The ThemedMixin provides a this.theme function that returns (or that will return) the correct class for the theme provided to the widget. The top-level class name is changed to root in order to provide a predictable way to target the outer-node of a widget, this is a pattern used throughout widgets provided by @dojo/widgets.

Replace the contents of Banner.ts with the following

  1. File: src/widgets/Banner.ts
    import { WidgetBase } from '@dojo/framework/widget-core/WidgetBase';
  2. import { v } from '@dojo/framework/widget-core/d';
  3. import { theme, ThemedMixin } from '@dojo/framework/widget-core/mixins/Themed';
  4. import * as css from '../styles/banner.m.css';
  5. @theme(css)
  6. export default class Banner extends ThemedMixin(WidgetBase) {
  7. protected render() {
  8. return v('h1', {
  9. title: 'I am a title!',
  10. classes: this.theme(css.root)
  11. }, [ 'Biz-E-Bodies' ]);
  12. }
  13. }

Reminder
If you cannot see the application, remember to run dojo build -m dev -w -s to build the application and start the development server.

This Banner widget will now have access to the classes in banner.m.css and can receive a theme. We use the root class to ensure that the theme we create can easily target the correct node.

Notice that this file and other css files now have a .m.css file extension. This is to indicate to the Dojo build system that this file is a css-module and should be processed as such.

CSS Modules
CSS Modules is a technique to use scoped CSS classnames by default.

Create a new style sheet for the Banner widget named banner.m.css.

We will create an empty root class for now as our base theme does not require any styles to be added to the Banner widget.

  1. File: src/styles/banner.m.css
    .root {
  2. }

Now, let's look at changing the WorkerForm widget

Fixed Classes
Fixed classes apply styles that cannot be overridden by a theme, using a suffix is a convention that helps easily differentiate the intention of these classes.

WorkerForm already uses the ThemedMixin and has a workerForm class on its root node. Let’s change the workerForm class to a root class, and while we are there, we will create a rootFixed class too, and apply it to the root node. Classes that are not passed to theme cannot be changed or overridden via a theme, ensuring that structured or nested styles are not lost when a theme is used.

  1. File: src/widgets/WorkerForms.ts
    return v('form', {
  2. classes: [ this.theme(css.root), css.rootFixed ],
  3. onsubmit: this._onSubmit
  4. }, [

Replace all of the selectors containing .workerForm with the following rules in workerForm.m.css.

  1. File: src/styles/workerForm.m.css
    .root {
  2. margin-bottom: 40px;
  3. }
  4. .rootFixed {
  5. text-align: center;
  6. }
  7. .rootFixed fieldset,
  8. .rootFixed label {
  9. display: inline-block;
  10. text-align: left;
  11. }
  12. .rootFixed label {
  13. margin-right: 10px;
  14. }

If you open the application in a browser its appearance and behavior should be unchanged.

Now update worker.m.css and workerContainer.m.css to use .root and .rootFixed

  1. File: src/styles/workerContainer.m.css
    .rootFixed {
  2. display: flex;
  3. flex-flow: row wrap;
  4. justify-content: center;
  5. align-items: stretch;
  6. margin: 0 auto;
  7. width: 100%;
  8. }
  9. .root {
  10. }
  1. File: src/styles/worker.m.css.
    .root {
  2. margin: 0 10px 40px;
  3. max-width: 350px;
  4. min-width: 250px;
  5. flex: 1 1 calc(33% - 20px);
  6. position: relative;
  7. }
  8. .rootFixed {
  9. /* flip transform styles */
  10. perspective: 1000px;
  11. transform-style: preserve-3d;
  12. }
  13. All rules were originally in the .worker selector.

…and then update the associated widgets to use the new selectors.

  1. // WorkerContainer.ts
  2. // ...
  3. render() {
  4. // ...
  5. return v('div', {
  6. classes: [ this.theme(css.root), css.rootFixed ]
  7. }, workers);
  8. // ...
  9. }
  10. // Worker.ts
  11. // ...
  12. render() {
  13. // ...
  14. return v('div', {
  15. classes: [
  16. ...this.theme([ css.root, this._isFlipped ? css.reverse : null ]),
  17. css.rootFixed
  18. ]
  19. }, [
  20. // ...
  21. }

Next, we will start to create a theme.

Creating a Theme

Create a theme directory

Dojo provides a CLI command for creating a skeleton theme from existing Dojo widgets. To do this the command prompts the user to enter the name of the package that contains Dojo widgets and allows you to select specific widgets to include in the output.

At the command line run dojo create theme —name dojo

When prompted for the package to theme enter @dojo/widgets and then type N to indicate their are no more packages. You should now be presented with a list of widgets from @dojo/widgets that can be scaffolded. For this demo we need to select button and text-input using the arrow keys and space to select each one, press enter to complete the process.

This should have created a themes directory in the project’s src directory, this will be where the custom theme is created.

Dojo uses the concept of a key to be able to look up and load classnames for a theme, it is a composite of the package name and the widget name joined by /. These keys are used as the keys to object that is exported from the main theme.ts.

Theme the Worker widget

In order to theme the Worker widget, we need to create worker.m.css within our themes/dojo directory and use it within theme.ts. As mentioned above, the naming of the key of the exported theme must match the name from the project’s package.json and the name of the widget’s style sheet joined by a forward slash (/).

Let’s start by creating a red Worker and observe our theme being applied correctly.

  1. /* worker.m.css */
  2. .root {
  3. background: red;
  4. }

So for theme.ts, you need to add the import for the worker.m.css and the theme itself to the exported object using the key biz-e-corp/worker.

  1. File: src/themes/dojo/theme.ts
    import * as worker from './worker.m.css';
  2. import * as textInput from './@dojo/widgets/text-input/text-input.m.css';
  3. import * as button from './@dojo/widgets/button/button.m.css';
  4. export default {
  5. 'dojo-theming-tutorial/worker': worker,
  6. '@dojo/widgets/text-input': textInput,
  7. '@dojo/widgets/button': button
  8. };

To apply a theme to a widget, simply pass the theme as a property to widgets to have the ThemedMixin applied. To ensure that the entire application applies the theme it needs to be passed to all the themed widgets in our application. This can become problematic when an application uses a mixture of themed and non-themed widgets, or uses a widget from a third party, meaning that there is no guarantee that the theme will be propagated as required.

What is a registry?
A registry provides a mechanism to inject external payloads into widgets throughout the application tree. To learn more, take a look at the container tutorial and registry tutorial.

However an application can automatically inject a theme from the registry to every themed widget in an application tree. First, create a themeInjector using the registerThemeInjector function by passing a registry instance and theme. This will return a handle to the themeInjector that can be used to change the theme using themeInjector.set(), which will invalidate all themed widgets in the application tree and re-render using the new theme!

Update our main.ts file to import our theme and create a themeInjector.

  1. File: src/main.ts
    import renderer from '@dojo/framework/widget-core/vdom';
  2. import { w } from '@dojo/framework/widget-core/d';
  3. import { registerThemeInjector } from '@dojo/framework/widget-core/mixins/Themed';
  4. import { Registry } from '@dojo/framework/widget-core/Registry';
  5. import App from './widgets/App';
  6. import theme from './themes/dojo/theme';
  7. const registry = new Registry();
  8. registerThemeInjector(theme, registry);
  9. const r = renderer(() => w(App, {}));
  10. r.mount({ domNode: document.querySelector('my-app') as HTMLElement, registry });

Open the application in your web browser and see that the Worker backgrounds are now red.

Use variables and complete the Worker theme

The Dojo build system supports new CSS features such as css-custom-properties by using PostCSS to process our .m.css files. We can use these new CSS features to add variables to worker.m.css and complete its theme.Let’s create themes/dojo/variables.css (notice that this file does not have a .m.css extension as it is not a css-module file).

  1. File: src/themes/dojo/variables.css
    :root {
  2. --font: 'arial';
  3. --container: #f9f9f9;
  4. --body: #000;
  5. --card: #fff;
  6. --accent: #0071f2;
  7. --secondary-accent: #075cbe;
  8. --component-accent: #f4f4f4;
  9. --input-border: #d6dde3;
  10. --padding: 32px;
  11. --spacing: 16px;
  12. }

In the above code you can see that we have created a number of CSS Custom Properties to be used within our theme and wrapped them in a :root selector which makes them available on the global scope within our css-modules.To use them, we can @import the variables.css file and use the var keyword to assign a css-custom-property to a css rule.

Now we will use these variables in our themes worker.m.css to create our fully themed Worker.

  1. File: src/themes/dojo/worker.m.css
    @import './variables.css';
  2. .root {
  3. width: 320px;
  4. height: 370px;
  5. box-sizing: border-box;
  6. margin: var(--spacing);
  7. position: relative;
  8. }
  9. .image {
  10. background: var(--component-accent);
  11. border-radius: 50%;
  12. width: 250px;
  13. height: 250px;
  14. margin-bottom: var(--padding);
  15. }
  16. .imageSmall {
  17. background: var(--component-accent);
  18. border-radius: 50%;
  19. width: 72px;
  20. height: 72px;
  21. display: inline-block;
  22. margin-right: 24px;
  23. }
  24. .workerFront, .workerBack {
  25. position: absolute;
  26. top: 0;
  27. bottom: 0;
  28. left: 0;
  29. right: 0;
  30. border: 1px solid var(--component-accent);
  31. }
  32. .workerFront {
  33. box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.35);
  34. padding: var(--padding);
  35. text-align: center;
  36. }
  37. .workerBack {
  38. box-shadow: 0 20px 35px 0 rgba(0, 0, 0, 0.15);
  39. background: var(--card);
  40. color: var(--body);
  41. }
  42. .heading {
  43. background: var(--accent);
  44. color: var(--card);
  45. padding: 24px;
  46. }
  47. .generalInfo {
  48. display: inline-block;
  49. vertical-align: top;
  50. line-height: 24px;
  51. }
  52. .generalInfo .label {
  53. display: none;
  54. }
  55. .tasks strong {
  56. background: var(--secondary-accent);
  57. color: var(--card);
  58. display: block;
  59. font-size: 14px;
  60. padding: 14px 24px;
  61. text-transform: uppercase;
  62. font-weight: 400;
  63. }
  64. .task {
  65. padding: 12px 24px;
  66. border-bottom: 1px solid var(--component-accent);
  67. }

Theming Dojo widgets

Thus far in this tutorial, we have themed our custom Worker widget, but we have not targeted Dojo widgets that are contained within our application. To demonstrate the styling of Dojo widgets, we will theme the workerForm widget as it contains both DOM nodes and Dojo widgets.

Let's create workerForm.m.css

  1. File: src/themes/dojo/workerForm.m.css
    @import './variables.css';
  2. .root {
  3. font-family: var(--font);
  4. padding: 30px;
  5. box-sizing: border-box;
  6. font-weight: bold;
  7. }
  8. .nameLabel {
  9. font-size: 14px;
  10. }

And include it in theme.ts

  1. File: src/themes/dojo/theme.ts
    import * as worker from './worker.m.css';
  2. import * as workerForm from './workerForm.m.css';
  3. export default {
  4. 'dojo-theming-tutorial/worker': worker,
  5. 'dojo-theming-tutorial/workerForm': workerForm,
  6. };

This should be familiar from theming the Worker in the previous section. To theme the Dojo TextInput within our WorkerForm, we need to add to the skeleton text-input.m.css theme created by @dojo/cli-create-theme, this is already exported from theme.ts.

  1. File: src/themes/dojo/@dojo/widgets/text-input/text-input.m.css
    @import './../../../variables.css';
  2. .root {
  3. margin-right: 10px;
  4. display: inline-block;
  5. composes: nameLabel from './../../../workerForm.m.css';
  6. text-align: left;
  7. }
  8. .input {
  9. height: 38px;
  10. border: 1px solid var(--input-border);
  11. box-sizing: border-box;
  12. border-bottom-color: color(var(--input-border) blackness(60%));
  13. padding: 8px;
  14. min-width: 230px;
  15. }

Notice the styling rule for the .root selector? Here we are introducing another powerful part of the Dojo theming system, composes. Composes originates in the css-module specification, allowing you to apply styles from one class selector to another. Here we are specifying that the root of a TextInput (the label text in this case), should appear the same as the nameLabel class in our WidgetForm. This approach can be very useful when creating multiple themes from a baseTheme and avoids repetitive redefinition of style rules.

In your web browser you will see the TextInput widgets at the top of the form have been styled.

Add the following theme styles to the button.m.css theme resource

  1. File: src/themes/dojo/@dojo/widgets/button/button.m.css
    @import './../../../variables.css';
  2. .root {
  3. height: 38px;
  4. border: 1px solid var(--input-border);
  5. background: var(--card);
  6. color: var(--accent);
  7. font-size: 16px;
  8. font-weight: bold;
  9. padding: 8px;
  10. min-width: 150px;
  11. vertical-align: bottom;
  12. }

Summary

In this tutorial, we learned:

  • How to create a theme
  • How to apply a theme to both Dojo and custom widgets using the themeInjector.
  • How to leverage functionality from the Dojo theming system to apply variables
  • How to use composes to share styles between components
    You can download the completed demo application from this tutorial.