User Interface Basics
The user interface of NativeScript mobile apps consists of
pages. Typically, the design of the user interface is developed
and stored in XML
files, styling is done via CSS
and the business logic is developed and stored in
JavaScript
or TypeScript
files. When
you develop the user interface of your app, you can implement
each application screen in a separate page or implement your
application screens on a single page with a tab view. For each
page, you need to have a separate XML
file that
holds the layout of the page. For each XML
file
that NativeScript parses, the framework also looks for a
JavaScript
or TypeScript
file with the
same name and executes the business logic inside it.
Declare the Home Page
Each NativeScript app must have a home page that loads when you
launch the app. You need to explicitly set the home page for
your app by calling the run
method of the
Application
module and pass NavigationEntry
with the desired
moduleName
.
The NativeScript navigation framework looks for an
XML
file with the specified name, loads it and
navigates to the respective page. If NativeScript discovers a
JavaScript
or TypeScript
file with the
same name, it executes the code inside it.
import { Application } from "@nativescript/core";
// Start the application. Don't place any code after this line as it will not be executed on iOS.
Application.run({ moduleName: "my-page" });
import { Application } from "@nativescript/core";
// Start the application. Don't place any code after this line as it will not be executed on iOS.
Application.run({ moduleName: "my-page" });
Note: Before NativeScript 4.0.0 the
start
method automatically created an underlying rootFrame
instance and wrapped your page. The newrun
method will set up the root element of the provided module as application root element. This effectively means that apart fromPage
, you can now have other roots of your app likeTabView
andSideDrawer
. Thestart
is now marked as deprecated.
Navigate to a Page
You can navigate between pages with the
navigate
method of the
Frame
class. The
Frame
class represents the logical unit that is responsible for
navigation between different pages. With NativeScript 4 and
above each app can have one or more frames. To get a reference
to a frame we can use
Frame.getFrameById
method. Detailed information about navigation can be found in
the
dedicated article.
When you trigger navigation, NativeScript looks for an
XML
file with the specified name, loads it and
navigates to the respective page. If NativeScript discovers a
JavaScript
or TypeScript
file with the
same name, it executes the code inside it.
import { Frame } from "@nativescript/core";
const frame = Frame.getFrameById("myFrame");
// Navigate to page called “my-page”
frame.navigate("my-page");
import { Frame } from "@nativescript/core";
const frame = Frame.getFrameById("myFrame");
// Navigate to page called “my-page”
frame.navigate("my-page");
<Frame id="myFrame"/>
Paths are relative to the application root. In the example above, NativeScript looks for a
my-page.xml
file in the app directory of your project (e.g.app/my-page.xml
).
Passing Binding Context while Navigating
You could provide bindingContext
automatically
while navigating to a page. This will give you a simple way to
make the context become the bindingContext
of the
page on navigation. The way to do that is to set up the
bindingContext
property, which points to your
custom view model, on navigate
method.
import { Frame } from "@nativescript/core";
const frame = Frame.getFrameById("myFrame");
const HelloWorldModel = require("./main-view-model").HelloWorldModel;
// Navigate to page called “my-page” and provide "bindingContext"
frame.navigate({
moduleName: "my-page",
bindingContext: new HelloWorldModel()
});
import { Frame } from "@nativescript/core";
const frame = Frame.getFrameById("myFrame");
import { HelloWorldModel } from "./main-view-model"
// Navigate to page called “my-page” and provide "bindingContext"
frame.navigate({
moduleName: "my-page",
bindingContext: new HelloWorldModel()
});
Passing and Receiving Custom Context
In cases where we want to pass a specific context and need more
control than the automated bindingContext
, you
could use the context
property in the
navigatedEntry
object. The navigated page can
obtain the passed context via the navigatedTo
event
and the
navigationContext
property.
Sending binding context from the main page.
// e.g. main-page.js
import { Frame } from "@nativescript/core";
const frame = Frame.getFrameById("myFrame");
// Navigate to page called “sub-page” and provide "bindingContext"
frame.navigate({
moduleName: "sub-page",
context: { title: "NativeScript is Awesome!"}
});
// e.g main-page.ts
import { Frame } from "@nativescript/core";
const frame = Frame.getFrameById("myFrame");
// Navigate to page called “sub-page” and provide "bindingContext"
frame.navigate({
moduleName: "sub-page",
context: { title: "NativeScript is Awesome!"}
});
Recieving context from sub-page
// sub-page.js
export function onNavigatedTo(args) {
const page = args.object;
page.bindingContext = page.navigationContext;
}
// sub-page.ts
import { Page } from "@nativescript/core";
export function onNavigatedTo(args) {
const page = <Page>args.object;
page.bindingContext = page.navigationContext;
}
<!-- sub-page.xml -->
<Page xmlns="http:/tns.xsd" navigatedTo="onNavigatedTo">
<Label text="" textWrap="true" />
</Page>
Execute Business Logic
When you have a JavaScript
or a
TypeScript
file in the same location with the same
name as your XML
file, NativeScript loads it
together with the XML
file. In this
JavaScript
or TypeScript
file you can
manage event handlers, bind context or execute additional
business logic.
In this example of main-page.xml
, your page
consists of a button. When you tap the button, the
buttonTap
function is triggered.
<Page>
<StackLayout>
<Label id="Label1" text="This is Label!" />
<Button text="This is Button!" tap="buttonTap" />
</StackLayout>
</Page>
This example demonstrates a simple counter app. The logic for
the counter is implemented in a main-page.js
or
main-page.ts
file.
import { getViewById } from "@nativescript/core";
let count = 0;
export function buttonTap(args) {
count++;
let button = args.object;
let parent = button.parent;
if (parent) {
let lbl = getViewById(parent, "Label1");
if (lbl) {
lbl.text = "You tapped " + count + " times!";
}
}
}
import { View, Label, getViewById, EventData } from "@nativescript/core";
let count = 0;
export function buttonTap(args: EventData) {
count++;
let button = <View>args.object;
let parent = button.parent;
if (parent) {
let lbl = <Label>getViewById(parent, "Label1");
if (lbl) {
lbl.text = "You tapped " + count + " times!";
}
}
}
To access variables or functions from the user interface, you
need to declare them in the exports
object in the
module. NativeScript sets each attribute value in the XML
declaration to a respective property or an event of the
component. If a respective property does not exist, NativeScript
sets the attribute value as an expando object.
User interface components
NativeScript provides a wide range of built-in user interface components—layouts and widgets. You can also create your own custom user interface components.
var Button = ...
...
export { Button };
export let Button = ...
The default content components
The top-level user interface components are content components like pages and layouts. These content components let you arrange your interactive user interface components in specific ways.
Page
Your application pages (or screens) are instances of the
page
class of the
Page
module. Typically, an app will consist of multiple application
screens.
You can execute some business logic when your page loads using
the pageLoaded
event. You need to set the
loaded
attribute for your page in your
main-page.xml
.
<Page loaded="pageLoaded">
<!-- Page content follows here -->
</Page>
You need to handle the business logic that loads in a
main-page.js
or main-page.ts
file.
export function pageLoaded(args) {
var page = args.object;
}
import { EventData, Page } from "@nativescript/core";
// Event handler for Page "loaded" event attached in main-page.xml
export function pageLoaded(args: EventData) {
// Get the event sender
const page = <Page>args.object;
}
TabView
With a
tabview
, you can avoid spreading your user interface across multiple
pages. Instead, you can have one page with multiple tabs.
The following sample main-page.xml
contains two
tabs with labels.
<Page loaded="pageLoaded">
<TabView id="tabView1">
<TabView.items>
<TabViewItem title="Tab 1">
<TabViewItem.view>
<Label text="This is Label in Tab 1" />
</TabViewItem.view>
</TabViewItem>
<TabViewItem title="Tab 2">
<TabViewItem.view>
<Label text="This is Label in Tab 2" />
</TabViewItem.view>
</TabViewItem>
</TabView.items>
</TabView>
</Page>
The respective main-page.js
or
main-page.ts
loads the first tab by its ID and
shows its contents.
import { getViewById } from "@nativescript/core";
export function pageLoaded(args) {
var page = args.object;
var tabView1 = getViewById(page, "tabView1");
tabView1.selectedIndex = 1;
}
import { EventData, Page, TabView, getViewById } from "@nativescript/core";
// Event handler for Page "loaded" event attached in main-page.xml
export function pageLoaded(args: EventData) {
// Get the event sender
var page = <Page>args.object;
var tabView1 = <TabView>getViewById(page, "tabView1");
tabView1.selectedIndex = 1;
}
ScrollView
Insert a ScrollView
inside your page to make the
page or the content enclosed in the
scrollView
scrollable.
<Page>
<ScrollView>
<!-- Scrollable content goes here -->
</ScrollView>
</Page>
StackLayout
Arrange the user interface components in your page in a
horizontal or vertical stack using StackLayout
and
its orientation
.
<Page>
<StackLayout orientation="horizontal">
<Label text="This is Label 1" />
<Label text="This is Label 2" />
</StackLayout>
</Page>
GridLayout
Arrange the user interface components in your page in a flexible
grid area using GridLayout
.
<Page>
<GridLayout rows="*, auto" columns="250, *">
<Label text="This is Label in row 0, col 0" />
<Label text="This is Label in row 0, col 1" col="1" />
<Label text="This is Label in row 1, col 0" row="1" />
<Label text="This is Label in row 1, col 1" row="1" col="1" />
<Label text="This is Label in row 0, col 0" rowSpan="2" colSpan="2" />
</GridLayout>
</Page>
WrapLayout
Arrange your user interface components in rows or columns until
the space is filled and then wrap them on a new row or column
using WrapLayout
. By default, if orientation is not
specified, WrapLayout
arranges items horizontally.
<Page>
<WrapLayout>
<Label text="This is Label 1" />
<Label text="This is Label 2" />
<Label text="This is Label 3" />
<Label text="This is Label 4" />
</WrapLayout>
</Page>
AbsoluteLayout
Arrange your user interface components by left/top coordinates
using AbsoluteLayout
.
<Page>
<AbsoluteLayout>
<Label text="This is Label 1" left="30" top="70" />
</AbsoluteLayout>
</Page>
Custom Components
You can define your own XML namespaces to create custom user interface components. The custom components can be created via XML files or via code-behind JS/TS implementation.
Code-only Custom Component
The page using the custom component
The sample main-page.xml
is using a custom
component defined in separate declarations in the
app/components/my-control.ts
file (or
*.js
if using plain JavaScript).
<!-- app/main-page.xml -->
<Page xmlns:customControls="components/my-control" navigatingTo="navigatingTo" class="page">
<customControls:MyControl />
</Page>
The custom component implementation
This sample custom component declared in
app/components/my-control.ts
or
app/components/my-control.js
exports the
MyControl
variable, which creates a simple counter
inside your main-page.xml
page.
// app/components/my-control.ts
import { Label, Button, StackLayout } from "@nativescript/core";
export class MyControl extends StackLayout {
constructor() {
super();
let counter: number = 0;
const lbl = new Label();
const btn = new Button();
btn.text = "Tap me!";
btn.on("tap", (args) => {
lbl.text = "Tap " + counter++;
});
this.addChild(lbl);
this.addChild(btn);
}
}
// app/components/my-control.js
var __extends = this.__extends || function (d, b) {
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
function __() { this.constructor = d; }
__.prototype = b.prototype;
d.prototype = new __();
};
import { Button, StackLayout, View, Label, getViewById, EventData } from "@nativescript/core";
var MyControl = (function (_super) {
__extends(MyControl, _super);
function MyControl() {
_super.call(this);
var counter = 0;
var lbl = new Label();
var btn = new Button();
btn.text = "Tap me!";
btn.on(Button.tapEvent, function (args) {
lbl.text = "Tap " + counter++;
});
this.addChild(lbl);
this.addChild(btn);
}
return MyControl;
})(StackLayout);
export { MyControl };
When referring to code-only components in your pages with an
xmlns
declaration, you should point it either to
the code file with the component implementation or to the folder
containing the files. In the latter case, you will have to add a
package.json
file in the folder so that the file
can be required properly.
XML-based Custom Component with a Code File
The page using the custom component
The sample main-page.xml
is using a custom
component my-control.xml
and the
my-control.ts
code-behind defined as a separate
files in the app/components
folder.
<!-- app/main-page.xml -->
<Page xmlns:comps="components" navigatingTo="navigatingTo">
<comps:my-control />
</Page>
// app/main-page.ts
import { EventData, Page } from '@nativescript/core';
import { HelloWorldModel } from './main-view-model';
export function navigatingTo(args: EventData) {
let page = <Page>args.object;
// the page binding context will be accerss in components/my-toolbar
page.bindingContext = new HelloWorldModel();
}
// app/main-page.js
var main_view_model_1 = require("./main-view-model");
export function navigatingTo(args) {
var page = args.object;
// the page binding context will be accerss in components/my-toolbar
page.bindingContext = new main_view_model_1.HelloWorldModel();
}
The custom component implementation
The custom component in
app/components/my-control.xml
defines a Button, a
Label with a related binding properties and tap function.
<!-- app/components/my-control.xml -->
<StackLayout class="p-20" loaded="onLoaded">
<Label text="This custom component binding is coming from the parent page" textWrap="true" />
<Label text="Tap the button (custom component)" class="h1 text-center"/>
<Button text="TAP" tap="{{ onTap }}" class="btn btn-primary btn-active"/>
<Label text="{{ message }}" class="h2 text-center" textWrap="true"/>
</StackLayout>
// app/components/my-control.ts
import { EventData } from "@nativescript/core";
export function onLoaded(args: EventData) {
console.log("Custom Component Loaded");
// you could also extend the custom component logic here e.g.:
// let stack = <StackLayout>args.view;
// stack.bindingContext = myCustomComponentViewModel;
}
// app/components/my-control.js
export function onLoaded(args) {
console.log("Custom Component Loaded");
// you could also extend the custom component logic here e.g.:
// let stack = args.view;
// stack.bindingContext = myCustomComponentViewModel;
}
The View Model used for bindings
The main-page
has a binding context set thought
view model (MVVM pattern). The binding context can be accessed
through the custom component as demonstrated.
// app/main-view-model.ts
import { Observable } from "@nativescript/core";
export class HelloWorldModel extends Observable {
private _counter: number;
private _message: string;
constructor() {
super();
// Initialize default values.
this._counter = 42;
this.updateMessage();
}
get message(): string {
return this._message;
}
set message(value: string) {
if (this._message !== value) {
this._message = value;
this.notifyPropertyChange('message', value)
}
}
public onTap() {
this._counter--;
this.updateMessage();
}
private updateMessage() {
if (this._counter <= 0) {
this.message = 'Hoorraaay! You unlocked the NativeScript clicker achievement!';
} else {
this.message = `${this._counter} taps left`;
}
}
}
// app/main-view-model.js
import { Observable } from "@nativescript/core";
function getMessage(counter) {
if (counter <= 0) {
return "Hoorraaay! You unlocked the NativeScript clicker achievement!";
} else {
return counter + " taps left";
}
}
export function createViewModel() {
var viewModel = new Observable();
viewModel.counter = 42;
viewModel.message = getMessage(viewModel.counter);
viewModel.onTap = function() {
this.counter--;
this.set("message", getMessage(this.counter));
}
return viewModel;
}
Dynamic Loading of Custom Components
Dynamic load of JavaScript/TypeScript component
Load a pure JavaScript component by finding it in the exports of the module. The component is specified by a path and its name. Then the code from the JavaScript file is executed.
import { Builder } from "@nativescript/core";
let myComponentInstance = Builder.load({
path: "~/components/my-control",
name: "MyControl"
});
import { Builder } from "@nativescript/core";
let myComponentInstance = Builder.load({
path: "~/components/my-control",
name: "MyControl"
});
Dynamic load of XML with JavaScript/TypeScript component
Load the XML file with JavaScript code-behind by finding the specified XML filename through the specified path in the exports of the modules. JavaScript file with the same name will be required and served as code-behind of the XML.
import { Builder } from "@nativescript/core";
let myComponentInstance = Builder.load({
path: "~/components/my-control",
name: "MyControl"
});
import { Builder } from "@nativescript/core";
let myComponentInstance = Builder.load({
path: "~/components/my-control",
name: "MyControl"
});
The UI builder will automatically load the CSS file with the same name as the component name and apply it to the specified page:
let myComponentInstance = Builder.load({ path: "~/components/my-control", name: "MyControl", page: yourPageInstancex });
let myComponentInstance = Builder.load({ path: "~/components/my-control", name: "MyControl", page: yourPageInstance });
Dynamic load and passing additional attributes
The attributes
option can be used to pass
additional arguments.
import { Builder } from "@nativescript/core";
let myComponentInstance = Builder.load({
path: "~/components/my-control",
name: "MyControl",
attributes: {
bindingContext: myBindingModel
}
});
import { Builder } from "@nativescript/core";
let myComponentInstance = Builder.load({
path: "~/components/my-control",
name: "MyControl",
attributes: {
bindingContext: myBindingModel
}
});
## Gestures
All [UI Gestures](/ui/gestures)
(gestures.md) can be defined in XML. For example:
```XML
<Page>
<Label text="Some text" tap="myTapHandler" />
</Page>
import { GestureEventData } from "@nativescript/core";
export function myTapHandler(args: GestureEventData) {
const context = args.view.bindingContext;
}
export function myTapHandler(args) {
const context = args.view.bindingContext;
}
Bindings
To set a binding for a property in the XML
, you can
use double curly brackets syntax. All about binding can be found
in the
data-binding article
Property Binding
This sample main-page.xml
contains a simple label
whose text will be populated when the page loads.
<Page>
<Label text="{{ myTitle }}" />
</Page>
The main-page.js
or main-page.ts
code
file sets a bindingContext
for the page. The
bindingContext
contains the custom property and its
value. When NativeScript parses main-page.xml
, it
will populate the custom name property with the value in the
bindingContext
.
export function navigatingTo(args) {
const page = args.object;
page.bindingContext = { myTitle: "NativeScript is Awesome!"};
}
import { EventData, Page } from "@nativescript/core";
export function navigatingTo(args: EventData) {
const page = <Page>args.object;
page.bindingContext = { myTitle: "NativeScript is Awesome!"};
}
NativeScript looks for the custom property in the
bindingContext
of the current component or thebindingContext
of its parents. By default, all bindings, defined in XML, are two-way bindings.
Event Binding
This sample main-page.xml
contains a button. The
text for the button and the event that the button triggers are
determined when the page loads from the matching
main-page.js
or main-page.ts
file.
<Page navigatingTo="navigatingTo">
<Button text="{{ myProperty }}" tap="{{ myFunction }}" />
</Page>
This sample main-page.js
or
main-page.ts
sets a bindingContext
for
the page. The bindingContext
contains the custom
property for the button text and its value and the custom
function that will be triggered when the button is tapped. When
NativeScript parses main-page.xml
, it will populate
the button text with the value in the
bindingContext
and will bind the custom function to
the tap event.
export function navigatingTo(args) {
const page = args.object;
page.bindingContext = {
myProperty: "Some text",
myFunction: () => {
// Your code
}
};
}
import { EventData, Page } from "@nativescript/core";
export function navigatingTo(args: EventData) {
const page = <Page>args.object;
page.bindingContext = {
myProperty: "Some text",
myFunction: () => {
// Your code
}
};
}
ListView Binding
You can use the double curly brackets syntax to bind the items
to a
listView
. You can also define a template with the
itemTemplate
property from which NativeScript will
create the items for your listView
.
Avoid accessing components by ID, especially when the component is part of a template. It is recommended that you use bindings to specify component properties.
NativeScript can create the items from a template when the
listView
loads inside your page. When you work with
templates and a listView
, keep in mind the scope of
the listView
and its items.
In this sample main-page.xml
, the ListView consists
of labels and each item will be created from a template. The
text of each label is the value of the name property of the
corresponding item.
<Page navigatingTo="navigatingTo">
<ListView id="listView1" items="{{ myItems }}">
<ListView.itemTemplate>
<Label id="label1" text="{{ name }}" />
</ListView.itemTemplate>
</ListView>
</Page>
The sample main-page.js
or
main-page.ts
populates the
bindingContext
for the page. In this case, the code
sets values for the name property for each label. Note that
because the ListView
and the Label have different
scopes, you can access ListView by ID from the page, but you
cannot access the Label by ID. The ListView
creates
a new Label
for every item.
import { getViewById } from "@nativescript/core";
export function navigatingTo(args) {
const page = args.object;
page.bindingContext = { myItems: [{ name: "Name1" }, { name: "Name2" }, { name: "Name3" }] };
// Will work!
let listView1 = getViewById(page, "listView1");
// Will not work!
let label1 = getViewById(page, "label1");
}
import { Button, StackLayout, View, Label, getViewById, EventData, ListView } from "@nativescript/core";
export function navigatingTo(args: EventData) {
const page = <Page>args.object;
page.bindingContext = { myItems: [{ name: "Name1" }, { name: "Name2" }, { name: "Name3" }] };
// Will work!
const listView1 = <ListView>getViewById(page, "listView1");
// Will not work!
const label1 = <Label>getViewById(page, "label1");
}
To show some inner collection items inside
ListView.itemTemplate
you can use a
Repeater:
<Page>
<ListView items="{{ myItems }}">
<ListView.itemTemplate>
<Repeater items="{{ mySubItems }}" />
</ListView.itemTemplate>
</ListView>
</Page>
Binding Expressions
To set an expression as a value of a property in the
XML
, you might as well go with the mustache syntax
here.
NativeScript reevaluates your expression on every property change of the
Observable
object set forbindingContext
. This binding is a one-way binding—from the view model to the user interface.
The following sample main-page.xml
shows how to set
an expression as the value for a label.
<Label text="{{ author ? 'by ' + author : '[no author]' }}" />
<Label text="{{ author || '[no author]' }}" />
Complex property paths
your.sub.property[name]
Logical not operator and comparators
!,<, >, <=, >=, ==, !=, ===, !==,||, &&
Unary and binary operators
+, -, *, /, %
Ternary operator
a ? b : c
Grouping
(a + b) * (c + d)
Constants
numbers, strings, null, undefined
Platform-specific declarations
To declare a platform-specific property value or
platform-specific component in the XML
, you can use
the following syntax:
Platform-specific property value
<Page>
<TextField ios:editable='False' android:editable='True' />
</Page>
Platform-specific component declaration
<Page>
<ios>
<TextField />
</ios>
<android>
<Label />
</android>
</Page>
You cannot nest platform tags!
Lowercase-dashed component declaration
Since the release of NativeScript 1.3, you can declare your UI using the lowercase-dashed syntax:
<page>
<scroll-view>
<stack-layout>
<label ctext="Label" />
<button text="Button" tap="tap" />
...