Using UI Editor And Classes

Just like in any webpage, a consistent mobile app needs a theming structure to help developers implement pages faster by reducing unnecessary overhead. It also helps designers to create a structural UI/UX to have a consistent experience in the app.

This document will focus on using class variables and overall importance of theming the application.

Difference between direct assignment and class assignment

There will always be a need to change properties of an UI element on runtime. Across the API reference and the guides on Smartface, you will encounter two types of assigning a property to an UI element.

  • Direct assignment

  • Using dispatch

Direct Assignment
Dispatch
Direct Assignment
function hideWrapperLayout() {
this.flexLayout1.height = 0;
this.flexLayout1.visible = false;
this.flexLayout1.positionType = FlexLayout.PositionType.RELATIVE;
this.layout.applyLayout();
}
function showWrapperLayout() {
this.flexLayout1.height = 300;
this.flexLayout1.visible = true;
this.flexLayout1.positionType = FlexLayout.PositionType.ABSOLUTE;
this.layout.applyLayout();
}
Dispatch
function hideWrapperLayout() {
this.flexLayout.dispatch({
type: "updateUserStyle",
userStyle: {
height: 0,
visible: false,
positionType: "RELATIVE"
}
});
this.layout.applyLayout();
}
function showWrapperLayout() {
this.flexLayout.dispatch({
type: "updateUserStyle",
userStyle: {
height: 300,
visible: true,
positionType: "ABSOLUTE"
}
});
this.layout.applyLayout();
}

Two methods aim towards the same goal, hiding or showing the given layout.

However, using direct assignment is considered as anti-pattern and discouraged unless you are assigning an attribute. Refer here for updated list of attributes.

Smartface uses underlying Context renderer for their UI rendering and keeping class based structure. By directly assigning those, context will not be informed about the change, therefore might cause unexpected behaviours like incorrect positioning or not applying the changes.

function setLabel() {
this.label1.text = "Smartface"; // This is correct, text is an attribute
this.label1.width = 100; // This is anti pattern, width is not an attribute
// Use dispatch like this to update the context and width of the element
this.label1.dispatch({
type: "updateUserStyle",
userStyle: {
width: 100
}
});
// ApplyLayout might be necessary, refer to applyLayout doc below
}

Theming Structure

Like in every framework or bare-bone css, Smartface has a theme structure to provide theming. Refer to below doc for detailed information about separating styles in themes:

By default, there are 2 distinct themes, plus your additional themes on a Smartface application:

  • defaultTheme

    • This is a default theming for Smartface applications to run. When Smartface releases updates, the contents in the defaultTheme will be overwritten with version defaults. Therefore, you should not change anything in this folder.

  • baseTheme

    • This is a base theme which your application will be using. You should place your global styles in this folder like:

      • Margin left-right

      • Heights

      • Helper styles like flexgrow ( relative and absolute )

    • If your application will have dark theme support or different skin support for e.g. halloween or holidays, separating color classes with dimension classes are crucial.

  • yourTheme

    • As explained in Using Themes in Apps doc, you can create your own themes to make your styles dynamic and interchangeable.

    • This theme should contain anything that falls under skin category like:

      • Fonts

      • Colors

      • Variables ( which will be explained on different section )

      • image names

You can create as much as different theme as you like. As long as you keep the same naming convention, switching themes will be a piece of cake !

As architect or project owner, your goal should be to hand over the extended theme to anyone without coding experience and they should be able to tweak with colors as much as they like to create different skins for your application. The developers should implement dimensional styles like width or height inside of baseTheme folder.

Style filing structure

When you create a style on UI editor, it can have three different naming:

  • By starting the name with # , It creates a separate file.

    • This notation is commonly used to separate page styles like #pgLogin on a different file. Use this when you are going to have a specific style for a page. Common examples are:

      • Aligning the dimensions of a specific element that only belongs to that page

      • When re-using a style, the predefined style might not be enough for your needs. You can create a page specific style for that purpose

  • By starting the name with .componentName , it creates a different file on components folder. Casing does not matter, but it is advised to use camel case for component names.

  • By typing anything like .smartface , it will create the style in common.json file. You should avoid that unless you are creating a global-wide style.

Purpose of common.json

Every theme folder will have its own common.json file. This file should only contain global theming like project-wide margin or height.

For example, the two classes below has a purpose of making the element full screen, in relative and absolute style. Since this doesn't have to do with theme, this can be considered as global-wide style and can be placed in common.json.

common.json
{
".flexGrow-relative": {
"height": null,
"width": null,
"flexProps": {
"flexGrow": 1
}
},
".flexGrow-absolute": {
"top": 0,
"bottom": 0,
"left": 0,
"right": 0,
"width": null,
"height": null,
"flexProps": {
"flexGrow": 1,
"positionType": "ABSOLUTE"
}
}
}

To learn more about . and - notations, refer to:

User properties

User properties is element-specific property setting without a class. The common case of user property is to make temporary changes for debugging purposes, like changing the background color of a troubling element to reveal its position on the device.

Ideally, no component or element should have any user property.

More info can be found at this doc.

Dynamically assigning or removing classes

After you created and used your class, you might want to add another class to the component on runtime, instead of using user properties.

Let's recreate the above example with classes.

Code
Style
Code
function hideWrapperLayout() {
this.flexLayout.dispatch({
type: "pushClassNames",
classNames: ".layout-hidden"
});
this.layout.applyLayout();
}
function showWrapperLayout() {
this.flexLayout.dispatch({
type: "pushClassNames",
classNames: ".layout-shown"
});
// Alternate method
this.flexLayout.dispatch({
type: "removeClassName",
className: ".layout-hidden" // Instead of pushing shown class, we removed hidden class
});
this.layout.applyLayout();
}
Style
{
".layout": {
"&-shown": {
"height": 300,
"visible": true,
"flexProps": {
"positionType": "ABSOLUTE"
}
},
"&-hidden": {
"height": 0,
"visible": false,
"flexProps": {
"positionType": "RELATIVE"
}
}
}
}

More information can be found at contxjs document.

Getting style properties from code

After styling an element, if you need value of the style, you should use getCombinedStyle utility.

Common use case is, when you specify height of a ListViewItem in its respective class, you will need that height value at the onRowHeight method. For this case, you should use getCombinedStyle. Example for ListViewItem height:

Theme File
Component Code
Theme File
themes/baseTheme/component/LviRow1Line.json
{
".lviRow1Line": {
"height": 80,
"flexProps": {
"justifyContent": "FLEX_START"
}
}
}
Component Code
scripts/components/LviRow1Line.ts
import LviRow1LineDesign from 'generated/my-components/LviRow1Line';
import { getCombinedStyle } from 'sf-extension-utils/lib/getCombinedStyle';
export default class LviRow1Line extends LviRow1LineDesign {
pageName?: string | undefined;
constructor(props?: any, pageName?: string) {
super(props);
this.pageName = pageName;
}
static getHeight(): number {
return getCombinedStyle('.lviRow1Line').height || 0;
}
}

More info about ListView and its structure can be found at following doc:

Example Login page with Best Practices

To demonstrate, we are going to create design of this login page in matter of minutes, using Smartface theming!

The components will be:

  • 1 Label for Big login text

  • 2 MaterialTextBox objects

  • 1 label for Forgot Password? text

  • 1 button for Login button

For maintenance purposes, developers should refrain using default names and change their page and element names accordingly.

After drag & drop on the UI editor in order, the UI of pgLogin should look like this:

Make sure that assignToPage value of elements are true, in order to use them in code.

Styling and wrappers

We have added our elements, but now there are no styling done. Before we head out to make font changes, let's adjust the dimensions and place our elements first.

When styling, you should refrain from using hardcoded dimensions, especially width. You should always consider different sizing of mobile devices.

You should take advantage of flexbox, since Smartface supports built-in flexbox by default. Practice it by playing flexboxfroggy game.

Page Specific Styles

As explained in the document above, any dimensional theme independent page-specific variables should be styled like #pageName.

For left and right padding, we should add a global style at common.json file, to use it everywhere later on.

You can also override and add desired values to .sf-page style to reduce overhead. However, in our case, we might need pages without horizontal paddings, so we keep it in a different style.

Keeping the naming convention and by only doing dimensional styling, our current page files look like this

Page Theme
Page Markup
Common.json
Page Code
Page Theme
themes/baseTheme/styles/pages/PgLogin.json
{
"#pgLogin": {
"paddingTop": 40,
"&-lblLoginText": {
"height": 90,
"textAlignment": "MIDCENTER"
},
"&-lblForgotPassword": {
"textAlignment": "MIDRIGHT"
},
"&-btnLogin": {
"marginTop": 16
}
}
}
Page Markup
.ui/pgLogin.pgx
{
"components": [
{
"className": ".sf-page #pgLogin .padding-vertical",
"id": "e891b86d",
"initialized": true,
"props": {
"children": [
"42b807d1",
"b7d28cff",
"01ee-3b7d-fae4-a0fe",
"e8d0-e028-be62-a7e2",
"80c4-be9c-a3f5-3e3b",
"0855-e9ce-5840-6caf",
"0cff-66df-b749-b9f8"
],
"isRemovable": true,
"name": "pgLogin",
"orientation": "PORTRAIT",
"parent": null,
"safeAreaEnabled": true
},
"type": "Page",
"userProps": {
"orientation": "PORTRAIT",
"safeAreaEnabled": true
},
"version": "6.15.1"
},
{
"className": ".sf-statusBar",
"id": "42b807d1",
"props": {
"children": [],
"isRemovable": false,
"name": "statusBar",
"parent": "e891b86d"
},
"type": "StatusBar",
"userProps": {
"visible": true
}
},
{
"className": ".sf-headerBar",
"id": "b7d28cff",
"props": {
"children": [],
"isRemovable": false,
"name": "headerBar",
"parent": "e891b86d",
"title": "Page1"
},
"type": "HeaderBar",
"userProps": {
"title": "Page1",
"visible": true
}
},
{
"className": ".sf-label #pgLogin-lblLoginText",
"id": "01ee-3b7d-fae4-a0fe",
"props": {
"children": [],
"name": "lblLoginText",
"parent": "e891b86d",
"text": "Login",
"usePageVariable": true
},
"type": "Label",
"userProps": {
"testId": "31XoZqypi",
"text": "Login",
"usePageVariable": true
}
},
{
"className": ".flexLayout .materialTextBox-wrapper",
"hiddenComponent": false,
"id": "e8d0-e028-be62-a7e2",
"initialized": false,
"props": {
"children": [],
"name": "mtbUsername",
"parent": "e891b86d",
"usePageVariable": true
},
"source": {
"page": "__library__",
"type": "materialTextBox",
"id": "574a-10da-b765-48e5"
},
"type": "FlexLayout",
"userProps": {
"flex": {
"positionType": 0
},
"flexProps": {
"positionType": "RELATIVE"
},
"left": 0,
"testId": "GBgEp8H6g",
"top": 0,
"usePageVariable": true
}
},
{
"className": ".flexLayout .materialTextBox-wrapper",
"hiddenComponent": false,
"id": "80c4-be9c-a3f5-3e3b",
"initialized": false,
"props": {
"children": [],
"name": "mtbPassword",
"parent": "e891b86d",
"usePageVariable": true
},
"source": {
"page": "__library__",
"type": "materialTextBox",
"id": "574a-10da-b765-48e5"
},
"type": "FlexLayout",
"userProps": {
"flex": {
"positionType": 0
},
"flexProps": {
"positionType": "RELATIVE"
},
"left": 0,
"testId": "2VkXyj8Sk",
"top": 0,
"usePageVariable": true
}
},
{
"className": ".sf-label #pgLogin-lblForgotPassword",
"id": "0855-e9ce-5840-6caf",
"props": {
"children": [],
"name": "lblForgotPassword",
"parent": "e891b86d",
"text": "Forgot Password?",
"usePageVariable": true
},
"type": "Label",
"userProps": {
"testId": "cxb0GrKoG",
"text": "Forgot Password?",
"usePageVariable": true
}
},
{
"className": ".sf-button #pgLogin-btnLogin",
"id": "0cff-66df-b749-b9f8",
"props": {
"children": [],
"name": "btnLogin",
"parent": "e891b86d",
"text": "Login",
"usePageVariable": true
},
"type": "Button",
"userProps": {
"testId": "lU2lMxhdz",
"text": "Login",
"usePageVariable": true
}
}
]
}
Common.json
theme/baseTheme/styles/common.json
{
".padding": {
"&-vertical": {
"paddingLeft": 25,
"paddingRight": 25
}
}
}
Page Code
scripts/pages/pgLogin.ts
import PgLoginDesign from 'generated/pages/pgLogin';
export default class PgLogin extends PgLoginDesign {
constructor() {
super();
this.onShow = onShow.bind(this, this.onShow.bind(this));
this.onLoad = onLoad.bind(this, this.onLoad.bind(this));
}
initMaterialTextBoxes() {
this.mtbUsername.options = {
hint: "Username"
}
this.mtbPassword.options = {
hint: "Password"
}
this.mtbPassword.materialTextBox.isPassword = true;
}
}
function onShow(superOnShow: () => void) {
superOnShow();
}
function onLoad(superOnLoad: () => void) {
superOnLoad();
this.initMaterialTextBoxes();
}

Since MaterialTextBox is a special component, it should be initialized from code. More info is located at the document below

Creating a Theme and skinning

Since we are done with adjusting elements, we can change their color, font and other values.

First, create a theme by following the steps on this doc:

I have named it as smartfaceLightTheme, to specify the dark theme support.

On UI Editor, you can switch the theme on the go!

Change the active theme from UI editor and add following styles from UI editor:

Since our design doesn't have headerBar, we should hide it. We should also change color of statusBar to white.

PgLogin
StatusBar
HeaderBar
PgLogin
themes/smartfaceLightTheme/styles/pages/PgLogin.json
{
"#pgLogin": {
"&-lblLoginText": {
"textColor": "#000000",
"font": {
"size": 35
}
},
"&-lblForgotPassword": {
"textColor": "#000000",
"font": {
"size": 12
}
},
"&-btnLogin": {
"backgroundColor": "#000000"
}
}
}
StatusBar
themes/smartfaceLightTheme/styles/defaults/StatusBar.json
{
".sf-statusBar": {
"masksToBounds": true,
"android": {
"color": "#FFFFFF"
},
"style": "DEFAULT",
"visible": true
}
}
HeaderBar
themes/smartfaceLightTheme/styles/defaults/HeaderBar.json
{
".sf-headerBar": {
"masksToBounds": true,
"visible": false,
"backgroundColor": "rgba( 0, 161, 241, 1 )",
"titleColor": "rgba( 255, 255, 255, 1 )",
"itemColor": "rgba( 255, 255, 255, 1 )"
}
}

For statusBar and headerBar, or any other default component:

  • Create a new folder defaults in themes/smartfaceLightTheme/styles/ folder

  • copy the components you want to overwrite fromthemes/defaultTheme/styles/defaults/ folder to the newly created folder and overwrite it to your will!

Any skin value should be defined in its own theme.

Result should look like this:

Dark theme support

By following this method, you can also have dark theme in your application. For small scale applications, create another theme and adjust the skin values accordingly. Here is where separating dimensional and skin theming difference starts to matter.

Keep in mind that MaterialTextBox.json file is also copied to darkTheme folder

HeaderBar
StatusBar
Page
MaterialTextBox
HeaderBar
themes/smartfaceDarkTheme/styles/defaults/HeaderBar.json
{
".sf-headerBar": {
"masksToBounds": true,
"visible": false,
"backgroundColor": "rgba( 0, 161, 241, 1 )",
"titleColor": "rgba( 255, 255, 255, 1 )",
"itemColor": "rgba( 255, 255, 255, 1 )"
}
}
StatusBar
themes/smartfaceDarkTheme/styles/defaults/StatusBar.json
{
".sf-statusBar": {
"masksToBounds": true,
"android": {
"color": "#000000"
},
"style": "LIGHTCONTENT",
"visible": true
}
}
Page
themes/smartfaceDarkTheme/styles/pages/PgLogin.json
{
"#pgLogin": {
"backgroundColor": "#000000",
"&-lblLoginText": {
"textColor": "#FFFFFF",
"font": {
"size": 35
}
},
"&-lblForgotPassword": {
"textColor": "#FFFFFF",
"font": {
"size": 12
}
},
"&-btnLogin": {
"backgroundColor": "#FFFFFF",
"textColor": "#000000"
}
}
}
MaterialTextBox
themes/smartfaceDarkTheme/styles/MaterialTextBox.json
{
".materialTextBox": {
"ellipsizeMode": "END",
"textAlignment": "MIDLEFT",
"textColor": "rgba( 255,255,255,1 )",
"errorColor": "rgba( 255,0,0,1 )",
"hintTextColor": "rgba( 255,255,255,1 )",
"cursorColor": "rgba( 0,161,241,1 )",
"selectedHintTextColor": "rgba( 255,255,255,1 )",
"lineColor": {
"normal": "rgba( 255,255,255,1 )",
"selected": "rgba( 255,255,255,1 )"
},
"font": {
"size": 14,
"family": "Default"
},
"labelsFont": {
"size": 14,
"family": "Default"
}
},
".materialTextBox-rightLayout": {
"marginLeft": 1,
"marginRight": 1,
"paddingBottom": 4,
"flexProps": {
"alignItems": "FLEX_END",
"justifyContent": "FLEX_END",
"flexGrow": 1
},
"&-rightLabel": {
"textColor": "rgba( 119,119,119,1 )",
"textAlignment": "MIDRIGHT",
"font": {
"size": 12,
"family": "Default"
}
},
"+Device:Device.os === \"Android\"": {
"marginLeft": 4,
"marginRight": 4
}
},
".materialTextBox-wrapper": {
"height": null,
"width": null,
"marginLeft": -1,
"marginRight": -1,
"+Device:Device.os === \"Android\"": {
"height": 81,
"marginBottom": -8,
"marginLeft": -4,
"marginRight": -4
}
},
".materialTextBox-wrapper-dropArrow": {
"flexProps": {
"positionType": "ABSOLUTE"
},
"right": 2,
"height": 30,
"width": 30,
"top": 20,
"image": "arrowbottom.png",
".hidden": {
"visible": false
}
}
}

Heads Up

This method to switch between light/dark theme is intended for simple applications and requires the style contents to be created in the both theme folder. This method is not recommended for large scale application.

For larger application, variabling method should be used like explained below.

Handling Fonts

Your application may require different fonts, and font rendering can be different by platform and the font itself. To upload your font, refer to this doc:

Mock example of handling fonts for above login page:

Light Theme
Dark Theme
Light Theme
themes/smartfaceLightTheme/styles/textStyleCatalog.json
{
".login": {
"textColor": "#000000",
".forgotPassword": {
"font": {
"size": 12
}
},
".bigLoginText": {
"font": {
"size": 35
}
}
}
}
Dark Theme
themes/smartfaceDarkTheme/styles/textStyleCatalog.json
{
".login": {
"textColor": "#FFFFFF",
".forgotPassword": {
"font": {
"size": 12
}
},
".bigLoginText": {
"font": {
"size": 35
}
}
}
}

Keep in mind that . notation inherits the values from its parent. Therefore, in this example, both .login.forgotPassword and .login.bigLoginText will have the textColor

On the styling side for large scale applications, it is recommended to create a different style folder and gather all the fonts there, especially if you have a Style Catalog taken from design applications like Zeplin.

Handling Colors

To handle colors in a better way, we should use variables.json like mentioned about. Also, by using this method, we no longer have a need to create different json file for pages. These files should be deleted:

  • themes/smartfaceLightTheme/styles/pages/PgLogin.json

  • themes/smartfaceDarkTheme/styles/pages/PgLogin.json

  • themes/smartfaceLightTheme/styles/textStyleCatalog.json

  • themes/smartfaceDarkTheme/styles/textStyleCatalog.json

MaterialTextBox is an exception, it should be located under the first-most created theme. The values in baseTheme will be overridden!

Also, create those files:

  • textStyleCatalog.json file under themes/baseTheme/styles folder.

We are not going to delete headerbar and statusbar theme files, since it contains properties other than colors.

When we created themes, there are two new files named variables.json are created for us. Add those in them:

Light Theme
Dark Theme
Light Theme
themes/smartfaceLightTheme/styles/variables.json
{
"mainText": "#000000",
"background": "#FFFFFF"
}
Dark Theme
themes/smartfaceDarkTheme/styles/variables.json
{
"mainText": "#FFFFFF",
"background": "#000000"
}

Now we have out two simple variables, we may head to make necessary changes over our files. We need to adapt our theme files. Keep in mind that we are going to change files in baseTheme:

Login Page
TextStyleCatalog - Base
MaterialTextBox - Light
MaterialTextBox - Dark
Login Page
themes/baseTheme/styles/pages/PgLogin.json
{
"#pgLogin": {
"&-lblLoginText": {
"height": 90,
"textAlignment": "MIDCENTER"
},
"paddingTop": 40,
"&-lblForgotPassword": {
"textAlignment": "MIDRIGHT"
},
"&-btnLogin": {
"marginTop": 16,
"backgroundColor": "${mainText}",
"textColor": "${background}"
}
}
}
TextStyleCatalog - Base
themes/baseTheme/styles/textStyleCatalog.json
{
".login": {
"textColor": "${mainText}",
".forgotPassword": {
"font": {
"size": 12
}
},
".bigLoginText": {
"font": {
"size": 35
}
}
}
}
MaterialTextBox - Light
themes/smartfaceLightTheme/styles/MaterialTextBox.json
{
".materialTextBox": {
"ellipsizeMode": "END",
"textAlignment": "MIDLEFT",
"textColor": "${mainText}",
"errorColor": "rgba( 255,0,0,1 )",
"hintTextColor": "${mainText}",
"cursorColor": "rgba( 0,161,241,1 )",
"selectedHintTextColor": "${mainText}",
"lineColor": {
"normal": "${mainText}",
"selected": "${mainText}"
},
"font": {
"size": 14,
"family": "Default"
},
"labelsFont": {
"size": 14,
"family": "Default"
}
},
".materialTextBox-rightLayout": {
"marginLeft": 1,
"marginRight": 1,
"paddingBottom": 4,
"flexProps": {
"alignItems": "FLEX_END",
"justifyContent": "FLEX_END",
"flexGrow": 1
},
"&-rightLabel": {
"textColor": "rgba( 119,119,119,1 )",
"textAlignment": "MIDRIGHT",
"font": {
"size": 12,
"family": "Default"
}
},
"+Device:Device.os === \"Android\"": {
"marginLeft": 4,
"marginRight": 4
}
},
".materialTextBox-wrapper": {
"height": null,
"width": null,
"marginLeft": -1,
"marginRight": -1,
"+Device:Device.os === \"Android\"": {
"height": 81,
"marginBottom": -8,
"marginLeft": -4,
"marginRight": -4
}
},
".materialTextBox-wrapper-dropArrow": {
"flexProps": {
"positionType": "ABSOLUTE"
},
"right": 2,
"height": 30,
"width": 30,
"top": 20,
"image": "arrowbottom.png",
".hidden": {
"visible": false
}
}
}
MaterialTextBox - Dark
themes/smartfaceLightTheme/styles/MaterialTextBox.json
{
".materialTextBox": {
"ellipsizeMode": "END",
"textAlignment": "MIDLEFT",
"textColor": "${mainText}",
"errorColor": "rgba( 255,0,0,1 )",
"hintTextColor": "${mainText}",
"cursorColor": "rgba( 0,161,241,1 )",
"selectedHintTextColor": "${mainText}",
"lineColor": {
"normal": "${mainText}",
"selected": "${mainText}"
},
"font": {
"size": 14,
"family": "Default"
},
"labelsFont": {
"size": 14,
"family": "Default"
}
},
".materialTextBox-rightLayout": {
"marginLeft": 1,
"marginRight": 1,
"paddingBottom": 4,
"flexProps": {
"alignItems": "FLEX_END",
"justifyContent": "FLEX_END",
"flexGrow": 1
},
"&-rightLabel": {
"textColor": "rgba( 119,119,119,1 )",
"textAlignment": "MIDRIGHT",
"font": {
"size": 12,
"family": "Default"
}
},
"+Device:Device.os === \"Android\"": {
"marginLeft": 4,
"marginRight": 4
}
},
".materialTextBox-wrapper": {
"height": null,
"width": null,
"marginLeft": -1,
"marginRight": -1,
"+Device:Device.os === \"Android\"": {
"height": 81,
"marginBottom": -8,
"marginLeft": -4,
"marginRight": -4
}
},
".materialTextBox-wrapper-dropArrow": {
"flexProps": {
"positionType": "ABSOLUTE"
},
"right": 2,
"height": 30,
"width": 30,
"top": 20,
"image": "arrowbottom.png",
".hidden": {
"visible": false
}
}
}

You should also change StatusBar styles:

StatusBar - Light
StatusBar - Dark
StatusBar - Light
themes/smartfaceLightTheme/styles/defaults/StatusBar.json
{
".sf-statusBar": {
"masksToBounds": true,
"android": {
"color": "${mainText}"
},
"style": "DEFAULT",
"visible": true
}
}
StatusBar - Dark
themes/smartfaceDarkTheme/styles/defaults/StatusBar.json
{
".sf-statusBar": {
"masksToBounds": true,
"android": {
"color": "${mainText}"
},
"style": "LIGHTCONTENT",
"visible": true
}
}

Changing Theme on Runtime

Let's add another label under Login button and name it lblChangeTheme . It can have the same style with Forgot Password text, so no additional styling is required. It will be aligned to left.

Login Page Code
Login Page Markup
Login Page Code
scripts/pages/pgLogin.ts
import PgLoginDesign from 'generated/pages/pgLogin';
import { ThemeService } from 'theme';
import { config } from 'settings.json';
export default class PgLogin extends PgLoginDesign {
private currentTheme = config.theme.currentTheme;
constructor() {
super();
this.onShow = onShow.bind(this, this.onShow.bind(this));
this.onLoad = onLoad.bind(this, this.onLoad.bind(this));
}
initMaterialTextBoxes() {
this.mtbUsername.options = {
hint: "Username"
}
this.mtbPassword.options = {
hint: "Password"
}
this.mtbPassword.materialTextBox.isPassword = true;
}
initChangeTheme() {
this.lblChangeTheme.onTouchEnded = () => {
const nextTheme = this.currentTheme.includes('Dark') ? 'smartfaceLightTheme' : 'smartfaceDarkTheme';
ThemeService.changeTheme(nextTheme);
this.currentTheme = nextTheme;
}
}
}
function onShow(superOnShow: () => void) {
superOnShow();
}
function onLoad(superOnLoad: () => void) {
superOnLoad();
this.initMaterialTextBoxes();
this.initChangeTheme();
}
Login Page Markup
{
"components": [
{
"className": ".sf-page #pgLogin .padding-vertical",
"id": "e891b86d",
"initialized": true,
"props": {
"children": [
"42b807d1",
"b7d28cff",
"01ee-3b7d-fae4-a0fe",
"e8d0-e028-be62-a7e2",
"80c4-be9c-a3f5-3e3b",
"0855-e9ce-5840-6caf",
"0cff-66df-b749-b9f8",
"f14e-2237-759b-cb90"
],
"isRemovable": true,
"name": "pgLogin",
"orientation": "PORTRAIT",
"parent": null,
"safeAreaEnabled": true
},
"type": "Page",
"userProps": {
"orientation": "PORTRAIT",
"safeAreaEnabled": true
},
"version": "6.15.1"
},
{
"className": ".sf-statusBar",
"id": "42b807d1",
"props": {
"children": [],
"isRemovable": false,
"name": "statusBar",
"parent": "e891b86d"
},
"type": "StatusBar",
"userProps": {}
},
{
"className": ".sf-headerBar",
"id": "b7d28cff",
"props": {
"children": [],
"isRemovable": false,
"name": "headerBar",
"parent": "e891b86d",
"title": "Page1"
},
"type": "HeaderBar",
"userProps": {
"title": "Page1",
"visible": false
}
},
{
"className": ".sf-label #pgLogin-lblLoginText .login.bigLoginText",
"id": "01ee-3b7d-fae4-a0fe",
"props": {
"children": [],
"name": "lblLoginText",
"parent": "e891b86d",
"text": "Login",
"usePageVariable": true
},
"type": "Label",
"userProps": {
"testId": "31XoZqypi",
"text": "Login",
"usePageVariable": true
}
},
{
"className": ".flexLayout .materialTextBox-wrapper",
"hiddenComponent": false,
"id": "e8d0-e028-be62-a7e2",
"initialized": false,
"props": {
"children": [],
"name": "mtbUsername",
"parent": "e891b86d",
"usePageVariable": true
},
"source": {
"page": "__library__",
"type": "materialTextBox",
"id": "574a-10da-b765-48e5"
},
"type": "FlexLayout",
"userProps": {
"flex": {
"positionType": 0
},
"flexProps": {
"positionType": "RELATIVE"
},
"left": 0,
"testId": "GBgEp8H6g",
"top": 0,
"usePageVariable": true
}
},
{
"className": ".flexLayout .materialTextBox-wrapper",
"hiddenComponent": false,
"id": "80c4-be9c-a3f5-3e3b",
"initialized": false,
"props": {
"children": [],
"name": "mtbPassword",
"parent": "e891b86d",
"usePageVariable": true
},
"source": {
"page": "__library__",
"type": "materialTextBox",
"id": "574a-10da-b765-48e5"
},
"type": "FlexLayout",
"userProps": {
"flex": {
"positionType": 0
},
"flexProps": {
"positionType": "RELATIVE"
},
"left": 0,
"testId": "2VkXyj8Sk",
"top": 0,
"usePageVariable": true
}
},
{
"className": ".sf-label #pgLogin-lblForgotPassword .login.forgotPassword",
"id": "0855-e9ce-5840-6caf",
"props": {
"children": [],
"name": "lblForgotPassword",
"parent": "e891b86d",
"text": "Forgot Password?",
"usePageVariable": true
},
"type": "Label",
"userProps": {
"testId": "cxb0GrKoG",
"text": "Forgot Password?",
"usePageVariable": true
}
},
{
"className": ".sf-button #pgLogin-btnLogin",
"id": "0cff-66df-b749-b9f8",
"props": {
"children": [],
"name": "btnLogin",
"parent": "e891b86d",
"text": "Login",
"usePageVariable": true
},
"type": "Button",
"userProps": {
"testId": "lU2lMxhdz",
"text": "Login",
"usePageVariable": true
}
},
{
"className": ".sf-label .login.forgotPassword",
"id": "f14e-2237-759b-cb90",
"props": {
"children": [],
"name": "lblChangeTheme",
"parent": "e891b86d",
"text": "Change Theme",
"usePageVariable": true
},
"type": "Label",
"userProps": {
"testId": "xE30i1PuB",
"text": "Change Theme",
"usePageVariable": true
}
}
]
}

This is only an illustration. Normally, theme change code should be exposed from scripts/theme.ts file.

Also, ThemeService.changeTheme function is executed on main thread, therefore it is sync. You may place loading sign before and after the code.

It is recommended to save the current theme information into device storage. That way, devices will remember what theme they were in. More information can be found at the doc below:

Zeplin Integration

Did you know that Smartface has Zeplin Integration ? You can easily convert the styles and text catalog to smartface compatible syntax.

More Best Practices & Recipes

Under Smartface organization, there are more best practice examples located here